chromium/ash/wm/snap_group/snap_group_unittest.cc

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

#include "ash/wm/snap_group/snap_group.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <sstream>
#include <vector>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/accessibility/test_accessibility_controller_client.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/accelerators.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/style/close_button.h"
#include "ash/style/system_toast_style.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/test_shell_delegate.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_test_api.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/desks/templates/saved_desk_save_desk_button.h"
#include "ash/wm/desks/templates/saved_desk_test_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_drop_target.h"
#include "ash/wm/overview/overview_focus_cycler.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_grid_test_api.h"
#include "ash/wm/overview/overview_group_item.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_item_base.h"
#include "ash/wm/overview/overview_item_view.h"
#include "ash/wm/overview/overview_metrics.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_test_base.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/overview/overview_types.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/overview/overview_window_drag_controller.h"
#include "ash/wm/overview/scoped_overview_transform_window.h"
#include "ash/wm/snap_group/snap_group_constants.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_metrics.h"
#include "ash/wm/snap_group/snap_group_observer.h"
#include "ash/wm/snap_group/snap_group_test_util.h"
#include "ash/wm/splitview/layout_divider_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_divider_view.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_setup_view.h"
#include "ash/wm/splitview/split_view_test_util.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/test/test_non_client_frame_view_ash.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "ash/wm/window_cycle/window_cycle_controller.h"
#include "ash/wm/window_cycle/window_cycle_item_view.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_cycle/window_cycle_view.h"
#include "ash/wm/window_mini_view.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/wm_metrics.h"
#include "ash/wm/workspace/multi_window_resize_controller.h"
#include "ash/wm/workspace/phantom_window_controller.h"
#include "ash/wm/workspace/workspace_event_handler.h"
#include "ash/wm/workspace/workspace_window_resizer.h"
#include "ash/wm/workspace/workspace_window_resizer_test_api.h"
#include "ash/wm/workspace_controller_test_api.h"
#include "base/check_op.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chromeos/ui/base/display_util.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/window.h"
#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
#include "ui/base/hit_test.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/display_switches.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/test/test_widget_observer.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_modality_controller.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/window_move_client.h"

namespace ash {

namespace {

using chromeos::WindowStateType;
using testing::ElementsAre;
using ui::mojom::CursorType;
using WindowCyclingDirection = WindowCycleController::WindowCyclingDirection;

void SwitchToTabletMode() {
  TabletModeControllerTestApi test_api;
  test_api.DetachAllMice();
  test_api.EnterTabletMode();
}

void ExitTabletMode() {
  TabletModeControllerTestApi().LeaveTabletMode();
}

// Maximize the snapped window which will exit the split view session. This is
// used in preparation for the next round of testing.
void MaximizeToClearTheSession(aura::Window* window) {
  WindowState* window_state = WindowState::Get(window);
  window_state->Maximize();
  SplitViewOverviewSession* split_view_overview_session =
      RootWindowController::ForWindow(window)->split_view_overview_session();
  EXPECT_FALSE(split_view_overview_session);
}

// Drag the given group `item` to the `screen_location`. This is added before
// the event handling of the middle seam is done.
void DragGroupItemToPoint(OverviewItemBase* item,
                          const gfx::Point& screen_location,
                          ui::test::EventGenerator* event_generator,
                          bool by_touch_gestures,
                          bool drop) {
  DCHECK(item);

  gfx::Point location =
      gfx::ToRoundedPoint(item->target_bounds().CenterPoint());
  // TODO(michelefan): Use the center point of the `overview_item` after
  // implementing or defining the event handling for the middle seam area.
  location.Offset(/*delta_x=*/5, /*delta_y=*/5);
  event_generator->set_current_screen_location(location);
  if (by_touch_gestures) {
    event_generator->PressTouch();
    event_generator->MoveTouchBy(50, 0);
    event_generator->MoveTouch(screen_location);
    if (drop) {
      event_generator->ReleaseTouch();
    }
  } else {
    event_generator->PressLeftButton();
    Shell::Get()->cursor_manager()->SetDisplay(
        display::Screen::GetScreen()->GetDisplayNearestPoint(screen_location));
    event_generator->MoveMouseTo(screen_location);
    if (drop) {
      event_generator->ReleaseLeftButton();
    }
  }
}

// Activates the given 'window' hosted by an `OverviewGroupItem`.
void ActivateWindowInOverviewGroupItem(
    aura::Window* window,
    ui::test::EventGenerator* event_generator,
    bool by_touch_gestures) {
  ASSERT_TRUE(IsInOverviewSession());

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(window));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  const std::unique_ptr<OverviewItem>& selected_item =
      overview_items[0]->GetWindow() == window ? overview_items[0]
                                               : overview_items[1];
  ASSERT_EQ(window, selected_item->GetWindow());

  event_generator->set_current_screen_location(
      gfx::ToRoundedPoint(selected_item->target_bounds().CenterPoint()));
  if (by_touch_gestures) {
    event_generator->PressTouch();
    event_generator->ReleaseTouch();
  } else {
    event_generator->ClickLeftButton();
  }
}

// Gets a point to drag `window` by the header.
gfx::Point GetDragPoint(aura::Window* window) {
  auto* frame = NonClientFrameViewAsh::Get(window);
  views::test::RunScheduledLayout(frame);
  return frame->GetHeaderView()->GetBoundsInScreen().CenterPoint();
}

// Verifies the windows and divider of `snap_group` are on `display_id`.
void VerifySnapGroupOnDisplay(SnapGroup* snap_group, const int64_t display_id) {
  aura::Window* root = snap_group->window1()->GetRootWindow();
  EXPECT_EQ(root, snap_group->window2()->GetRootWindow());
  EXPECT_EQ(root, snap_group->snap_group_divider()->GetRootWindow());
  EXPECT_EQ(display_id,
            display::Screen::GetScreen()->GetDisplayNearestWindow(root).id());
}

void ResizeDividerTo(ui::test::EventGenerator* event_generator,
                     gfx::Point resize_point) {
  const gfx::Point divider_center(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  event_generator->MoveMouseTo(divider_center);
  event_generator->PressLeftButton();
  // Resize with at least 2 steps to simulate the real CUJ of dragging the
  // mouse. The default test EventGenerator sends only the start and end points
  // which is an abrupt jump between points.
  event_generator->MoveMouseTo(resize_point, /*count=*/2);
  event_generator->ReleaseLeftButton();
}

void LongTapAt(ui::test::EventGenerator* event_generator,
               const gfx::Point& point) {
  ui::GestureEvent long_press(
      point.x(), point.y(), 0, base::TimeTicks::Now(),
      ui::GestureEventDetails(ui::EventType::kGestureLongPress));
  event_generator->Dispatch(&long_press);
}

// Simulates dragging `window` to `point`, verifying that it gets dragged.
void DragWindowTo(ui::test::EventGenerator* event_generator,
                  aura::Window* window,
                  gfx::Point point,
                  bool release) {
  wm::ActivateWindow(window);
  event_generator->MoveMouseTo(GetDragPoint(window));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(point);
  ASSERT_TRUE(WindowState::Get(window)->is_dragged());
  if (release) {
    event_generator->ReleaseLeftButton();
  }
}

// -----------------------------------------------------------------------------
// SnapGroupTestBase:

class SnapGroupTestBase : public OverviewTestBase {
 public:
  template <typename... TaskEnvironmentTraits>
  explicit SnapGroupTestBase(TaskEnvironmentTraits&&... traits)
      : OverviewTestBase(std::forward<TaskEnvironmentTraits>(traits)...) {}
  SnapGroupTestBase(const SnapGroupTestBase&) = delete;
  SnapGroupTestBase& operator=(const SnapGroupTestBase&) = delete;
  ~SnapGroupTestBase() override = default;

  std::unique_ptr<aura::Window> CreateAppWindowWithMinSize(gfx::Size min_size) {
    std::unique_ptr<aura::Window> window =
        CreateAppWindow(gfx::Rect(800, 600), chromeos::AppType::SYSTEM_APP,
                        kShellWindowId_Invalid, new TestWidgetDelegateAsh);
    auto* custom_frame = static_cast<TestNonClientFrameViewAsh*>(
        NonClientFrameViewAsh::Get(window.get()));
    custom_frame->SetMinimumSize(min_size);
    return window;
  }
};

}  // namespace

// -----------------------------------------------------------------------------
// FasterSplitScreenTest:

// Test fixture to verify faster split screen feature.

class FasterSplitScreenTest : public SnapGroupTestBase {
 public:
  FasterSplitScreenTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{features::kSnapGroup,
                              features::kOsSettingsRevampWayfinding},
        /*disabled_features=*/{});
  }
  FasterSplitScreenTest(const FasterSplitScreenTest&) = delete;
  FasterSplitScreenTest& operator=(const FasterSplitScreenTest&) = delete;
  ~FasterSplitScreenTest() override = default;

  // AshTestBase:
  void SetUp() override {
    OverviewTestBase::SetUp();
    WindowCycleList::SetDisableInitialDelayForTesting(true);
  }

 protected:
  base::HistogramTester histogram_tester_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests the behavior in existing partial overview, i.e. overview -> drag to
// snap.
TEST_F(FasterSplitScreenTest, OldPartialOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // Enter overview, then drag to snap. Test we start partial overview.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  // Note `DragGroupItemToPoint()` just drags the overview item, not necessarily
  // the group item.
  // TODO(b/344877340): Consider renaming this to `DragOverviewItemToPoint()`.
  auto* event_generator = GetEventGenerator();
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(0, 0),
                       event_generator, /*by_touch_gestures=*/false,
                       /*drop=*/true);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  // Verify that split view overview session is active and the grid bounds are
  // updated, but the faster splitscreen setup UI is *not* shown.
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());
  // Select the other window in overview, test we end overview and split view.
  ClickOverviewItem(event_generator, w2.get());
  VerifyNotSplitViewOrOverviewSession(w1.get());
  MaximizeToClearTheSession(w1.get());

  // Enter overview, then drag `w1` to snap, then drag the other `w2` to snap.
  // Test we start and end overview and split view correctly.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(0, 0),
                       event_generator, /*by_touch_gestures=*/false,
                       /*drop=*/true);
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());
  DragGroupItemToPoint(GetOverviewItemForWindow(w2.get()),
                       GetWorkAreaBounds().top_right(), event_generator,
                       /*by_touch_gestures=*/false,
                       /*drop=*/true);
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  VerifyNotSplitViewOrOverviewSession(w1.get());
  MaximizeToClearTheSession(w1.get());
  // We need to maximize both windows so that overview isn't started upon
  // conversion to tablet mode.
  // TODO(b/327269057): Investigate tablet mode conversion.
  MaximizeToClearTheSession(w2.get());

  // Test that tablet mode works as normal.
  SwitchToTabletMode();
  EXPECT_FALSE(IsInOverviewSession());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());
}

// Tests that if the user disables the pref for snap window suggestions, we
// don't start faster splitview.
TEST_F(FasterSplitScreenTest, DisableSnapWindowSuggestionsPref) {
  PrefService* pref =
      Shell::Get()->session_controller()->GetActivePrefService();

  pref->SetBoolean(prefs::kSnapWindowSuggestions, false);
  ASSERT_FALSE(pref->GetBoolean(prefs::kSnapWindowSuggestions));

  // Snap a window. Test we don't start overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifyNotSplitViewOrOverviewSession(w1.get());

  // Turn on the pref. Test we start overview.
  pref->SetBoolean(prefs::kSnapWindowSuggestions, true);
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(w1.get());
}

TEST_F(FasterSplitScreenTest, Basic) {
  // Create two test windows, snap `w1`. Test `w1` is snapped and excluded from
  // overview while `w2` is in overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  auto* overview_controller = Shell::Get()->overview_controller();
  EXPECT_TRUE(
      overview_controller->overview_session()->IsWindowInOverview(w2.get()));

  // Select `w2` from overview. Test `w2` auto snaps.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  WaitForOverviewExitAnimation();
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_FALSE(
      RootWindowController::ForWindow(w1.get())->split_view_overview_session());

  // Create a new `w3` and snap it to the left. Test it doesn't start overview.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(overview_controller->InOverviewSession());

  // Create a new `w4` and snap it to the right. Test it doesn't start overview.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w4.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w4.get())->GetStateType());

  // Test all the other window states remain the same.
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w3.get())->GetStateType());

  // Enter overview normally. Test that no windows widget will not show.
  ToggleOverview();
  auto* overview_grid = GetOverviewGridForRoot(w1->GetRootWindow());
  EXPECT_FALSE(overview_grid->no_windows_widget());
  EXPECT_FALSE(overview_grid->split_view_setup_widget());
}

// Tests that on one window snapped, `SnapGroupController` starts
// `SplitViewOverviewSession` (snap group creation session).
TEST_F(FasterSplitScreenTest, CloseSnappedWindowEndsSplitViewOverviewSession) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // Snap `w1` to the left. Test that we are in split view overview, excluding
  // `w1` and taking half the screen.
  SnapOneTestWindow(w1.get(),
                    /*state_type=*/WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Close `w1`. Test that we end overview.
  w1.reset();
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}

// Tests that faster split screen can only start with certain snap action
// sources.
TEST_F(FasterSplitScreenTest, SnapActionSourceLimitations) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  struct {
    WindowSnapActionSource snap_action_source;
    bool should_show_partial_overview;
  } kTestCases[]{
      {WindowSnapActionSource::kSnapByWindowLayoutMenu,
       /*should_show_partial_overview=*/true},
      {WindowSnapActionSource::kDragWindowToEdgeToSnap,
       /*should_show_partial_overview=*/true},
      {WindowSnapActionSource::kLongPressCaptionButtonToSnap,
       /*should_show_partial_overview=*/true},
      {WindowSnapActionSource::kLacrosSnapButtonOrWindowLayoutMenu,
       /*should_show_partial_overview=*/true},
      {WindowSnapActionSource::kKeyboardShortcutToSnap,
       /*should_show_partial_overview=*/false},
      {WindowSnapActionSource::kSnapByWindowStateRestore,
       /*should_show_partial_overview=*/false},
      {WindowSnapActionSource::kSnapByFullRestoreOrDeskTemplateOrSavedDesk,
       /*should_show_partial_overview=*/false},
  };

  for (const auto test_case : kTestCases) {
    SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                      chromeos::kDefaultSnapRatio,
                      test_case.snap_action_source);
    EXPECT_EQ(test_case.should_show_partial_overview, IsInOverviewSession());
    MaximizeToClearTheSession(w1.get());
  }
}

TEST_F(FasterSplitScreenTest, CycleSnap) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* window_state = WindowState::Get(w1.get());

  // Cycle snap to the left.
  const WindowSnapWMEvent cycle_snap_primary(WM_EVENT_CYCLE_SNAP_PRIMARY);
  window_state->OnWMEvent(&cycle_snap_primary);
  auto* overview_controller = Shell::Get()->overview_controller();
  EXPECT_FALSE(overview_controller->InOverviewSession());

  // Cycle snap to the right.
  const WindowSnapWMEvent cycle_snap_secondary(WM_EVENT_CYCLE_SNAP_SECONDARY);
  window_state->OnWMEvent(&cycle_snap_secondary);
  EXPECT_FALSE(overview_controller->InOverviewSession());
}

TEST_F(FasterSplitScreenTest, EndSplitViewOverviewSession) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Drag `w1` out of split view. Test it ends overview.
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseBy(10, 10);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());

  // Snap then minimize the window. Test it ends overview.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  WMEvent minimize_event(WM_EVENT_MINIMIZE);
  WindowState::Get(w1.get())->OnWMEvent(&minimize_event);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());

  // Snap then close the window. Test it ends overview.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  w1.reset();
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

TEST_F(FasterSplitScreenTest, ResizeSplitViewOverviewAndWindow) {
  UpdateDisplay("900x600");
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  const gfx::Rect initial_bounds(w1->GetBoundsInScreen());

  // Drag the right edge of the window to resize the window and overview at the
  // same time. Test that the bounds are updated.
  const gfx::Point start_point(w1->GetBoundsInScreen().right_center());
  auto* generator = GetEventGenerator();
  generator->set_current_screen_location(start_point);

  // Resize to less than 1/3. Test we don't end overview.
  const auto drag_point1 = gfx::Point(
      GetWorkAreaBounds().width() * chromeos::kOneThirdSnapRatio - 10,
      start_point.y());
  generator->DragMouseTo(drag_point1);
  gfx::Rect expected_window_bounds(initial_bounds);
  expected_window_bounds.set_width(drag_point1.x());
  EXPECT_EQ(expected_window_bounds, w1->GetBoundsInScreen());
  VerifySplitViewOverviewSession(w1.get());

  // Resize to greater than 2/3. Test we don't end overview.
  const auto drag_point2 = gfx::Point(
      GetWorkAreaBounds().width() * chromeos::kTwoThirdSnapRatio + 10,
      start_point.y());
  generator->DragMouseTo(drag_point2);
  expected_window_bounds.set_width(drag_point2.x());
  EXPECT_EQ(expected_window_bounds, w1->GetBoundsInScreen());
  VerifySplitViewOverviewSession(w1.get());
}

// Tests that drag to snap window -> resize window -> snap window again restores
// to the default snap ratio. Regression test for b/315039407.
TEST_F(FasterSplitScreenTest, ResizeThenDragToSnap) {
  // Create `w2` first, as `w1` will be created on top and we want to drag it.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  // Drag to snap `w1` to 1/2.
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(0, 100);
  WindowState* window_state = WindowState::Get(w1.get());
  EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state->GetStateType());
  const gfx::Rect work_area(GetWorkAreaBounds());
  const gfx::Rect snapped_bounds(0, 0, work_area.width() / 2,
                                 work_area.height());
  EXPECT_EQ(snapped_bounds, w1->GetBoundsInScreen());

  // Resize `w1` to an arbitrary size not 1/2.
  event_generator->set_current_screen_location(snapped_bounds.right_center());
  event_generator->DragMouseBy(100, 10);
  EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state->GetStateType());
  EXPECT_NE(snapped_bounds, w1->GetBoundsInScreen());

  // Drag `w1` to unsnap and skip overview pairing.
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseBy(10, 10);
  EXPECT_FALSE(IsInOverviewSession());
  EXPECT_EQ(WindowStateType::kNormal, window_state->GetStateType());
  EXPECT_NE(snapped_bounds, w1->GetBoundsInScreen());

  // Drag to snap `w1` again. Test it snaps to 1/2.
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(0, 100);
  EXPECT_EQ(snapped_bounds, w1->GetBoundsInScreen());

  // Resize `w1` to an arbitrary size not 1/2 again.
  event_generator->set_current_screen_location(snapped_bounds.right_center());
  event_generator->DragMouseBy(-100, 10);
  EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state->GetStateType());
  EXPECT_NE(snapped_bounds, w1->GetBoundsInScreen());

  // Drag to snap `w2`. Test it snaps to 1/2.
  event_generator->set_current_screen_location(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(0, 100);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_EQ(snapped_bounds, w2->GetBoundsInScreen());
}

TEST_F(FasterSplitScreenTest, ResizeAndAutoSnap) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  const gfx::Rect initial_bounds(w1->GetBoundsInScreen());
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());

  auto* generator = GetEventGenerator();
  generator->set_current_screen_location(
      w1->GetBoundsInScreen().right_center());
  const int drag_x = 100;
  generator->DragMouseBy(drag_x, 0);
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());

  gfx::Rect expected_window_bounds(initial_bounds);
  expected_window_bounds.set_width(initial_bounds.width() + drag_x);
  EXPECT_EQ(expected_window_bounds, w1->GetBoundsInScreen());

  gfx::Rect expected_autosnap_bounds(GetWorkAreaBounds());
  expected_autosnap_bounds.Subtract(w1->GetBoundsInScreen());
  EXPECT_EQ(expected_autosnap_bounds,
            GetOverviewGridBounds(w1->GetRootWindow()));

  // Create a window and test that it auto snaps.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w3.get())->GetStateType());
  const int divider_delta = IsSnapGroupEnabledInClamshellMode()
                                ? kSplitviewDividerShortSideLength / 2
                                : 0;
  expected_autosnap_bounds.Subtract(
      gfx::Rect(expected_window_bounds.top_right(),
                gfx::Size(divider_delta, GetWorkAreaBounds().height())));
  EXPECT_EQ(expected_autosnap_bounds, w3->GetBoundsInScreen());
}

// Verify the window focus behavior both when activing a window or skipping
// pairing in partial overview.
// 1. When activating a window in partial overview, the chosen window will be
// the activated one upon exit;
// 2. When skipping pairing in partial overview, the snapped window will still
// be the activated one if it was activated before entering
// `SplitViewOverviewSession`.
TEST_F(FasterSplitScreenTest, SnappedWindowFocusTest) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> w2(CreateAppWindow(gfx::Rect(200, 100)));
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(100, 100)));
  ASSERT_TRUE(wm::IsActiveWindow(w1.get()));

  auto* event_generator = GetEventGenerator();
  for (const bool skip_pairing : {true, false}) {
    SnapOneTestWindow(w1.get(), chromeos::WindowStateType::kSecondarySnapped,
                      chromeos::kDefaultSnapRatio);
    VerifySplitViewOverviewSession(w1.get());

    auto* w2_overview_item = GetOverviewItemForWindow(w2.get());
    EXPECT_TRUE(w2_overview_item);

    const auto w2_overview_item_bounds = w2_overview_item->target_bounds();
    const gfx::Point click_point =
        skip_pairing
            ? gfx::ToRoundedPoint(w2_overview_item_bounds.bottom_right()) +
                  gfx::Vector2d(20, 20)
            : gfx::ToRoundedPoint(w2_overview_item_bounds.CenterPoint());

    event_generator->MoveMouseTo(click_point);
    event_generator->ClickLeftButton();

    EXPECT_EQ(wm::IsActiveWindow(w1.get()), skip_pairing);
    EXPECT_FALSE(IsInOverviewSession());
    MaximizeToClearTheSession(w1.get());
  }
}

TEST_F(FasterSplitScreenTest, DragToPartialOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  ToggleOverview();
  OverviewSession* overview_session =
      OverviewController::Get()->overview_session();
  ASSERT_TRUE(overview_session);
  EXPECT_TRUE(overview_session->IsWindowInOverview(w1.get()));
  EXPECT_TRUE(overview_session->IsWindowInOverview(w2.get()));

  // Drag `w1` to enter partial overview.
  auto* event_generator = GetEventGenerator();
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(0, 0),
                       event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_TRUE(overview_session->IsWindowInOverview(w2.get()));

  // Select `w2`. Test it snaps and we end overview.
  ClickOverviewItem(event_generator, w2.get());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}

// Tests that when clicking or tapping on the empty area during faster split
// screen setup session, overview will end.
TEST_F(FasterSplitScreenTest, SkipPairingInOverviewWhenActivatingTheEmptyArea) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  ASSERT_EQ(1u, GetOverviewSession()->grid_list().size());

  auto* w2_overview_item = GetOverviewItemForWindow(w2.get());
  EXPECT_TRUE(w2_overview_item);
  const gfx::Point outside_point =
      gfx::ToRoundedPoint(
          w2_overview_item->GetTransformedBounds().bottom_right()) +
      gfx::Vector2d(20, 20);

  // Verify that clicking on an empty area in overview will exit the paring.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(outside_point);
  event_generator->ClickLeftButton();
  EXPECT_FALSE(IsInOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);

  // Verify that tapping on an empty area in overview will exit the paring.
  MaximizeToClearTheSession(w1.get());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  event_generator->MoveTouch(outside_point);
  event_generator->PressTouch();
  event_generator->ReleaseTouch();
  EXPECT_FALSE(IsInOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
}

// Tests that when clicking or tapping on the snapped window on the `HTCLIENT`
// or `HTCAPTION` area during faster split screen setup session, overview will
// end.
TEST_F(FasterSplitScreenTest, SkipPairingWhenActivatingTheSnappedWindow) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  aura::test::TestWindowDelegate delegate;

  auto* event_generator = GetEventGenerator();

  // Snap `w1`. Test that moving the mouse around won't end overview
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().CenterPoint());
  EXPECT_TRUE(IsInOverviewSession());
  MaximizeToClearTheSession(w1.get());

  // Build test cases to verify that overview will end when clicking or tapping
  // on the window caption or client area.
  struct {
    int window_component;
    bool is_click_event;
  } kTestCases[]{
      {/*window_component*/ HTCLIENT, /*is_click_event=*/true},
      {/*window_component*/ HTCAPTION, /*is_click_event=*/true},
      {/*window_component*/ HTCLIENT, /*is_click_event=*/false},
      {/*window_component*/ HTCAPTION, /*is_click_event=*/false},
  };

  for (const auto& test_case : kTestCases) {
    SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                      chromeos::kDefaultSnapRatio);
    VerifySplitViewOverviewSession(w1.get());
    delegate.set_window_component(test_case.window_component);
    if (test_case.is_click_event) {
      event_generator->ClickLeftButton();
    } else {
      event_generator->PressTouch();
      event_generator->ReleaseTouch();
    }
    EXPECT_FALSE(IsInOverviewSession());
    MaximizeToClearTheSession(w1.get());
  }
}

TEST_F(FasterSplitScreenTest, SkipPairingOnKeyEvent) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  ASSERT_EQ(1u, GetOverviewSession()->grid_list().size());

  // Test that Esc key exits overview.
  PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  OverviewController* overview_controller = OverviewController::Get();
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);

  // Test that Alt + Tab exits overview.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  PressAndReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  EXPECT_TRUE(Shell::Get()->window_cycle_controller()->IsCycling());
}

TEST_F(FasterSplitScreenTest, SkipPairingToast) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  auto* overview_grid = GetOverviewGridForRoot(w1->GetRootWindow());
  ASSERT_TRUE(overview_grid);

  auto* split_view_setup_view = overview_grid->GetSplitViewSetupView();
  ASSERT_TRUE(split_view_setup_view);
  LeftClickOn(split_view_setup_view->GetViewByID(
      SplitViewSetupView::kDismissButtonIDForTest));
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}

TEST_F(FasterSplitScreenTest, DontStartPartialOverviewAfterSkippingPairing) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Press Esc key to skip pairing.
  PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  OverviewController* overview_controller = OverviewController::Get();
  EXPECT_FALSE(overview_controller->InOverviewSession());

  // Snap `w2`. Since `w1` is snapped to primary, it doesn't start partial
  // overview. wm::ActivateWindow(w2.get());
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  EXPECT_EQ(WindowState::Get(w2.get())->GetStateType(),
            WindowStateType::kSecondarySnapped);
}

TEST_F(FasterSplitScreenTest, DontStartPartialOverviewAfterClosingWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Select `w2` to auto-snap it.
  ClickOverviewItem(GetEventGenerator(), w2.get());

  // Close `w2`, then open and snap a new `w3`. Test we don't start partial
  // overview.
  w2.reset();
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
}

TEST_F(FasterSplitScreenTest, StartPartialOverviewForMinimizedWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Now minimize `w1`, so visually there is no primary snapped window.
  WindowState::Get(w1.get())->Minimize();

  // Now snap `w2` to secondary. Since `w1` is minimized, it starts partial
  // overview.
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w2.get());
}

// Tests that when activating an already snapped window, cannot snap toast will
// not show by mistake. See b/323391799 for details.
TEST_F(FasterSplitScreenTest,
       DoNotShowCannotSnapToastWhenActivatingTheSnappedWindow) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  ASSERT_TRUE(WindowState::Get(w1.get())->IsSnapped());

  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  EXPECT_FALSE(IsInOverviewSession());

  wm::ActivateWindow(w1.get());
  EXPECT_FALSE(ToastManager::Get()->IsToastShown(kAppCannotSnapToastId));
}

TEST_F(FasterSplitScreenTest, DontStartPartialOverviewForFloatedWindow) {
  // Snap 2 test windows in place.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // To simulate the CUJ when a user selects a window from overview, activate
  // and snap `w2`.
  wm::ActivateWindow(w2.get());
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());

  // Create a 3rd floated window on top of `w2`.
  std::unique_ptr<aura::Window> floated_window = CreateAppWindow();
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(WindowState::Get(floated_window.get())->IsFloated());
  EXPECT_TRUE(
      w2->GetBoundsInScreen().Contains(floated_window->GetBoundsInScreen()));

  // Open a 4th window and snap it on top of `w1`. Test we don't start partial
  // overview.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

// Tests that partial overview will not be triggered if the window to be snapped
// is only one window for the active desk and on the current display.
TEST_F(FasterSplitScreenTest, DontStartPartiOverviewIfThereIsOnlyOneWindow) {
  UpdateDisplay("900x600, 901+0-900x600");
  ASSERT_EQ(Shell::GetAllRootWindows().size(), 2u);

  DesksController* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  Desk* desk1 = desks_controller->GetDeskAtIndex(1);

  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(10, 20, 200, 100)));

  // Create the 2nd window and move it to another desk.
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(100, 20, 200, 100)));
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w2.get()), desk0);
  desks_controller->MoveWindowFromActiveDeskTo(
      w2.get(), desk1, w2->GetRootWindow(),
      DesksMoveWindowFromActiveDeskSource::kShortcut);
  ASSERT_EQ(desks_util::GetDeskForContext(w2.get()), desk1);

  // Create the 3rd window on the 2nd display.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(1000, 20, 200, 100)));

  // Verify that snapping `w1` won't trigger partial overview.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_FALSE(IsInOverviewSession());
}

// Tests that only when there is a non-occluded window snapped on the opposite
// side should we skip showing partial overview on window snapped. This test
// focuses on the window layout setup **with** intersections.
TEST_F(FasterSplitScreenTest,
       OppositeSnappedWindowOcclusionWithIntersectionsTest) {
  UpdateDisplay("800x600");
  // Window Layout before snapping `w1` to the primary snapped position:
  // `w2` is snapped on the secondary snapped position;
  // `w3` is stacked above `w2` with intersections.
  //
  //                  +-----------+
  //          +-------|-+         |
  //          |       | |         |
  //          |   w3  | |   w2    |
  //          |       | |         |
  //          +-------|-+         |
  //                  +-----------+
  //
  // For the window layout setup above, we should show partial overview
  // when snapping `w1` by the desired snap action source.

  // Snap `w2` to the secondary snapped location without triggering faster split
  // screen to get window layout setup ready.
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  ASSERT_TRUE(w2->IsVisible());

  // Create `w3` with bounds that intersect with `w2`.
  std::unique_ptr<aura::Window> w3 =
      CreateAppWindow(gfx::Rect(350, 200, 150, 200));
  ASSERT_TRUE(w3->IsVisible());
  EXPECT_TRUE(w3->GetBoundsInScreen().Intersects(w2->GetBoundsInScreen()));

  // Create and snap `w1` to the primary snapped position and expect to trigger
  // the faster split screen setup.
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(w1->IsVisible());
  VerifySplitViewOverviewSession(w1.get());

  // Activate `w2` to bring it to the front and snap it to the primary
  // snapped location without triggering faster split screen in preparation for
  // the next round of testing. `w2` is fully visible now.
  wm::ActivateWindow(w2.get());
  SnapOneTestWindow(w2.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  EXPECT_FALSE(IsInOverviewSession());

  // Snap `w1` to secondary snapped position with desired snap action source to
  // trigger faster split screen setup, with `w1` occupying the primary snapped
  // position, partial overview shouldn't start.
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_FALSE(IsInOverviewSession());
}

// Tests that only when there is a non-occluded window snapped on the opposite
// side should we skip showing partial overview on window snapped. This test
// focuses on the window layout setup **without** intersections.
TEST_F(FasterSplitScreenTest,
       OppositeSnappedWindowOcclusionWithoutIntersectionsTest) {
  UpdateDisplay("800x600");
  // Window Layout before snapping `w1` to the primary snapped position:
  // `w2` is snapped on the secondary snapped position;
  // `w3` is stacked above `w2` without intersections.
  //
  //              +-----------+
  //              |    +---+  |
  //              |    | w3|  |
  //              |    +---+  |
  //              |    w2     |
  //              |           |
  //              +-----------+
  //
  // For the window layout setup above, we should show partial overview
  // when snapping `w1` by the desired snap action source.

  // Snap `w2` to the secondary snapped location without triggering faster split
  // screen to get window layout setup ready.
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  ASSERT_TRUE(w2->IsVisible());

  // Create `w3` with bounds confined by the bounds `w2`.
  std::unique_ptr<aura::Window> w3 =
      CreateAppWindow(gfx::Rect(550, 45, 50, 50));
  ASSERT_TRUE(w3->IsVisible());
  EXPECT_TRUE(w2->GetBoundsInScreen().Contains(w3->GetBoundsInScreen()));

  // Create and snap `w1` to the primary snapped position and expect to trigger
  // the faster split screen setup.
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(w1->IsVisible());
  VerifySplitViewOverviewSession(w1.get());

  // Activate `w2` to bring it to the front and snap it to the primary
  // snapped location without triggering faster split screen in preparation for
  // the next round of testing. `w2` is fully visible now.
  wm::ActivateWindow(w2.get());
  SnapOneTestWindow(w2.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  EXPECT_FALSE(IsInOverviewSession());

  // Snap `w1` to secondary snapped position with desired snap action source to
  // trigger faster split screen setup, with `w1` occupying the primary snapped
  // position, partial overview shouldn't start.
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_FALSE(IsInOverviewSession());
}

TEST_F(FasterSplitScreenTest, NoCrashOnDisplayChange) {
  UpdateDisplay("800x600,1000x600");
  display::test::DisplayManagerTestApi display_manager_test(display_manager());

  // Snap `window` on the second display. Test its bounds are updated.
  std::unique_ptr<aura::Window> window1(
      CreateTestWindowInShellWithBounds(gfx::Rect(900, 0, 100, 100)));
  std::unique_ptr<aura::Window> window2(
      CreateTestWindowInShellWithBounds(gfx::Rect(1000, 0, 100, 100)));
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  ASSERT_EQ(display_manager_test.GetSecondaryDisplay().id(),
            display::Screen::GetScreen()
                ->GetDisplayNearestWindow(window1.get())
                .id());
  const gfx::Rect work_area(
      display_manager_test.GetSecondaryDisplay().work_area());
  EXPECT_EQ(gfx::Rect(800, 0, work_area.width() / 2, work_area.height()),
            window1->GetBoundsInScreen());
  VerifySplitViewOverviewSession(window1.get());

  // Disconnect the second display. Test no crash.
  UpdateDisplay("800x600");
  base::RunLoop().RunUntilIdle();
}

// Tests that autosnapping a window with minimum size doesn't crash. Regression
// test for http://b/324483718.
TEST_F(FasterSplitScreenTest, SnapWindowWithMinimumSize) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  // 1 - Test min size > 1/3 scenario.
  // Set `w2` min size to be > 1/3 of the display width.
  aura::test::TestWindowDelegate delegate;
  std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
      &delegate, /*id=*/-1, gfx::Rect(800, 600)));
  int min_width = 396;
  delegate.set_minimum_size(gfx::Size(min_width, 0));

  // Snap `w1` to primary 2/3.
  WindowState* window_state = WindowState::Get(w1.get());
  const WindowSnapWMEvent snap_type(
      WM_EVENT_SNAP_PRIMARY, chromeos::kTwoThirdSnapRatio,
      /*snap_action_source=*/WindowSnapActionSource::kTest);
  window_state->OnWMEvent(&snap_type);
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());

  // Select `w2` from overview.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(
      gfx::ToRoundedPoint(GetOverviewItemForWindow(w2.get())
                              ->GetTransformedBounds()
                              .CenterPoint()));
  event_generator->ClickLeftButton();

  // Test it gets snapped at its minimum size.
  EXPECT_EQ(min_width, w2->GetBoundsInScreen().width());

  MaximizeToClearTheSession(w2.get());

  // 2 - Test min size > 1/2 scenario.
  // Set `w2` min size to be > 1/2 of the display width.
  min_width = 450;
  delegate.set_minimum_size(gfx::Size(min_width, 0));

  // Snap `w1` to primary 1/2.
  const WindowSnapWMEvent snap_default(
      WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
      /*snap_action_source=*/WindowSnapActionSource::kTest);
  window_state->OnWMEvent(&snap_default);
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());

  // Select `w2` from overview.
  event_generator->MoveMouseTo(
      gfx::ToRoundedPoint(GetOverviewItemForWindow(w2.get())
                              ->GetTransformedBounds()
                              .CenterPoint()));
  event_generator->ClickLeftButton();

  // Test it gets snapped at its minimum size.
  EXPECT_EQ(min_width, w2->GetBoundsInScreen().width());
}

// Tests we start partial overview if there's an opposite snapped window on
// another display.
TEST_F(FasterSplitScreenTest, OppositeSnappedWindowOnOtherDisplay) {
  UpdateDisplay("800x600,801+0-800x600");

  // Create 3 test windows, with `w3` on display 2.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(900, 0, 100, 100)));
  std::unique_ptr<aura::Window> w4(
      CreateAppWindow(gfx::Rect(1000, 0, 100, 100)));

  // Snap `w1` to primary on display 1.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  display::Screen* screen = display::Screen::GetScreen();
  auto display_list = screen->GetAllDisplays();
  ASSERT_EQ(display_list[0], screen->GetDisplayNearestWindow(w1.get()));

  // Test we start partial overview.
  EXPECT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(
      RootWindowController::ForWindow(w1.get())->split_view_overview_session());

  // Select `w2` to snap on the first display.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_EQ(display_list[0], screen->GetDisplayNearestWindow(w2.get()));

  // Snap `w3` to secondary on display 2.
  SnapOneTestWindow(w3.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_EQ(display_list[1], screen->GetDisplayNearestWindow(w3.get()));

  // Test we start partial overview since no window is snapped on display 2.
  EXPECT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(
      RootWindowController::ForWindow(w3.get())->split_view_overview_session());
}

// Tests that the snapped window bounds will be refreshed on display changes to
// preserve the snap ratio.
TEST_F(FasterSplitScreenTest, WindowBoundsRefreshedOnDisplayChanges) {
  UpdateDisplay("900x600");
  std::unique_ptr<aura::Window> window1(CreateAppWindow());
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(window1.get());
  ASSERT_EQ(WindowState::Get(window1.get())->snap_ratio(),
            chromeos::kTwoThirdSnapRatio);
  const auto work_area_bounds_1 = GetWorkAreaBounds();
  ASSERT_EQ(
      window1->GetBoundsInScreen(),
      gfx::Rect(0, 0, work_area_bounds_1.width() * chromeos::kTwoThirdSnapRatio,
                work_area_bounds_1.height()));

  UpdateDisplay("1200x600");
  VerifySplitViewOverviewSession(window1.get());
  EXPECT_EQ(WindowState::Get(window1.get())->snap_ratio(),
            chromeos::kTwoThirdSnapRatio);
  const auto work_area_bounds_2 = GetWorkAreaBounds();
  EXPECT_EQ(
      window1->GetBoundsInScreen(),
      gfx::Rect(0, 0, work_area_bounds_2.width() * chromeos::kTwoThirdSnapRatio,
                work_area_bounds_2.height()));
}

// Tests that the grid and faster splitview widget is updated on keyboard
// and work area bounds changes.
TEST_F(FasterSplitScreenTest, KeyboardAndWorkAreaBoundsChanges) {
  std::unique_ptr<aura::Window> window1(CreateAppWindow());
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  SnapOneTestWindow(window1.get(), chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(window1.get());

  // Show the virtual keyboard. Test we refresh the grid and widget bounds.
  SetVirtualKeyboardEnabled(true);
  auto* keyboard_controller = keyboard::KeyboardUIController::Get();
  keyboard_controller->ShowKeyboard(true);
  VerifySplitViewOverviewSession(window1.get());
  auto* root_window = window1->GetRootWindow();
  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(window1.get())->GetStateType());
  auto* overview_grid = GetOverviewGridForRoot(root_window);
  EXPECT_TRUE(
      GetOverviewGridBounds(root_window)
          .Contains(
              overview_grid->GetSplitViewSetupView()->GetBoundsInScreen()));

  // Hide the virtual keyboard. Test we refresh the grid and widget bounds.
  keyboard_controller->HideKeyboardByUser();
  VerifySplitViewOverviewSession(window1.get());
  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(window1.get())->GetStateType());
  EXPECT_TRUE(
      GetOverviewGridBounds(root_window)
          .Contains(
              overview_grid->GetSplitViewSetupView()->GetBoundsInScreen()));

  // Show the docked magnifier, which ends overview.
  auto* docked_magnifier_controller =
      Shell::Get()->docked_magnifier_controller();
  docked_magnifier_controller->SetEnabled(/*enabled=*/true);
  EXPECT_FALSE(IsInOverviewSession());
  // TODO(sophiewen): Consider testing no faster splitview widget.
}

// Test to verify that there will be no crash when dragging the snapped window
// out without resizing the window see crash in b/321111182.
TEST_F(FasterSplitScreenTest, NoCrashWhenDraggingTheSnappedWindow) {
  std::unique_ptr<aura::Window> window1(CreateAppWindow());
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(window1.get());

  std::unique_ptr<WindowResizer> resizer(CreateWindowResizer(
      window1.get(), gfx::PointF(), HTCAPTION, wm::WINDOW_MOVE_SOURCE_MOUSE));
  resizer->Drag(gfx::PointF(500, 100), /*event_flags=*/0);
  WindowState* window_state = WindowState::Get(window1.get());
  EXPECT_TRUE(window_state->is_dragged());
  resizer->CompleteDrag();
  EXPECT_FALSE(window_state->IsSnapped());
}

// Tests that after a minimized window gets auto-snapped, dragging the window
// won't lead to crash. See crash at http://b/324483508.
TEST_F(FasterSplitScreenTest,
       NoCrashWhenDraggingTheAutoSnappedWindowThatWasPreviouslyMinimized) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(100, 100, 100, 100)));
  WindowState* w2_window_state = WindowState::Get(w2.get());
  w2_window_state->Minimize();
  ASSERT_TRUE(w2_window_state->IsMinimized());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(w1.get());

  auto* w2_overview_item = GetOverviewItemForWindow(w2.get());
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(
      gfx::ToRoundedPoint(w2_overview_item->target_bounds().CenterPoint()));
  event_generator->ClickLeftButton();
  EXPECT_EQ(w2_window_state->GetStateType(),
            WindowStateType::kSecondarySnapped);

  std::unique_ptr<WindowResizer> resizer(CreateWindowResizer(
      w2.get(), gfx::PointF(), HTCAPTION, wm::WINDOW_MOVE_SOURCE_MOUSE));
  resizer->Drag(gfx::PointF(500, 100), /*event_flags=*/0);
  EXPECT_TRUE(w2_window_state->is_dragged());
  resizer->CompleteDrag();
  EXPECT_FALSE(w2_window_state->IsSnapped());
}

// Verifies the issue to snap a window in overview is working properly. see
// b/322893408.
TEST_F(FasterSplitScreenTest, EnterOverviewSnappingWindow) {
  std::unique_ptr<aura::Window> window1(
      CreateAppWindow(gfx::Rect(20, 20, 200, 100)));
  std::unique_ptr<aura::Window> windo2(
      CreateAppWindow(gfx::Rect(10, 10, 200, 100)));

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  ASSERT_TRUE(IsInOverviewSession());

  auto* overview_item = GetOverviewItemForWindow(window1.get());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(
      gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()));
  event_generator->PressLeftButton();
  event_generator->DragMouseTo(0, 0);
  event_generator->ReleaseLeftButton();
  EXPECT_TRUE(IsInOverviewSession());
}

// Verifies that there will be no crash when transitioning the
// `SplitViewOverviewSession` between clamshell and tablet mode.
TEST_F(FasterSplitScreenTest, ClamshellTabletTransitionOneSnappedWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  SwitchToTabletMode();
  EXPECT_TRUE(GetSplitViewDivider()->divider_widget());
  auto observed_windows = GetSplitViewDivider()->observed_windows();
  EXPECT_EQ(1u, observed_windows.size());
  EXPECT_EQ(w1.get(), observed_windows.front());

  TabletModeControllerTestApi().LeaveTabletMode();
}

TEST_F(FasterSplitScreenTest, ClamshellTabletTransitionTwoSnappedWindows) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  // Select the second window from overview to snap it.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_FALSE(GetSplitViewDivider()->divider_widget());

  SwitchToTabletMode();
  EXPECT_TRUE(GetSplitViewDivider()->divider_widget());
  auto observed_windows = GetSplitViewDivider()->observed_windows();
  EXPECT_EQ(2u, observed_windows.size());
  // TODO(b/312229933): Determine whether the order of `observed_windows_`
  // matters.
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), GetSplitViewDivider());

  TabletModeControllerTestApi().LeaveTabletMode();
}

// Tests that there will be no overlap between two windows on window layout
// setup complete. It used to happen because the minimum size of the window was
// never taken into account. See http://b/324631432 for more details.
TEST_F(FasterSplitScreenTest,
       NoOverlapAfterSnapRatioVariesToAccommodateForMinimumSize) {
  UpdateDisplay("900x600");

  std::unique_ptr<aura::Window> window1(CreateAppWindow());

  // Create `window2` with window minimum size above 1/3 of the work area.
  std::unique_ptr<aura::Window> window2(
      CreateAppWindowWithMinSize(gfx::Size(400, 200)));

  SnapOneTestWindow(window2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio);
  VerifySplitViewOverviewSession(window2.get());

  auto* item1 = GetOverviewItemForWindow(window1.get());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(
      gfx::ToRoundedPoint(item1->target_bounds().CenterPoint()));
  event_generator->ClickLeftButton();
  WaitForOverviewExitAnimation();
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());

  // Verify that the snap ratio of `window2` will be adjusted to accommodate for
  // the window minimum size.
  WindowState* window2_state = WindowState::Get(window2.get());
  ASSERT_TRUE(window2_state->snap_ratio());
  EXPECT_EQ(window2_state->GetStateType(), WindowStateType::kSecondarySnapped);
  EXPECT_GT(window2_state->snap_ratio().value(), chromeos::kOneThirdSnapRatio);

  // Verify that the auto snap ratio of `window1` will be adjusted as well.
  WindowState* window1_state = WindowState::Get(window1.get());
  ASSERT_TRUE(window1_state->snap_ratio());
  EXPECT_EQ(window1_state->GetStateType(), WindowStateType::kPrimarySnapped);
  EXPECT_LT(window1_state->snap_ratio().value(), chromeos::kTwoThirdSnapRatio);

  const int divider_delta = IsSnapGroupEnabledInClamshellMode()
                                ? kSplitviewDividerShortSideLength
                                : 0;
  // Both windows will fit within the work are with no overlap
  if (auto* snap_group_controller = SnapGroupController::Get()) {
    EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(window1.get(),
                                                             window2.get()));
    UnionBoundsEqualToWorkAreaBounds(window1.get(), window2.get(),
                                     GetTopmostSnapGroupDivider());
  } else {
    EXPECT_EQ(window1->GetBoundsInScreen().width() +
                  window2->GetBoundsInScreen().width() + divider_delta,
              GetWorkAreaBounds().width());
  }
}

// Tests that double tap to swap windows doesn't crash after transition to
// tablet mode (b/308216746).
TEST_F(FasterSplitScreenTest, NoCrashWhenDoubleTapAfterTransition) {
  // Use non-zero to start an animation, which will notify
  // `SplitViewOverviewSession::OnWindowBoundsChanged()`.
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  SwitchToTabletMode();
  EXPECT_TRUE(GetSplitViewDivider()->divider_widget());

  // Double tap on the divider. This will start a drag and notify
  // SplitViewOverviewSession.
  const gfx::Point divider_center =
      GetSplitViewDivider()
          ->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint();
  GetEventGenerator()->GestureTapAt(divider_center);
  GetEventGenerator()->GestureTapAt(divider_center);
}

TEST_F(FasterSplitScreenTest, BasicTabKeyNavigation) {
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  std::unique_ptr<aura::Window> window1(CreateAppWindow());

  const WindowSnapWMEvent snap_event(WM_EVENT_SNAP_PRIMARY,
                                     WindowSnapActionSource::kTest);
  WindowState::Get(window1.get())->OnWMEvent(&snap_event);
  ASSERT_TRUE(IsInOverviewSession());

  OverviewFocusCycler* focus_cycler = GetOverviewSession()->focus_cycler();

  // Tab until we get to the first overview item.
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, GetEventGenerator());
  const std::vector<std::unique_ptr<OverviewItemBase>>& overview_windows =
      GetOverviewItemsForRoot(0);
  EXPECT_EQ(overview_windows[0]->item_widget(),
            focus_cycler->GetOverviewFocusedView()->GetWidget());

  OverviewGrid* grid = GetOverviewSession()->grid_list()[0].get();

  // Tab to the toast dismiss button.
  PressAndReleaseKey(ui::VKEY_TAB);
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                SplitViewSetupView::kDismissButtonIDForTest),
            focus_cycler->GetOverviewFocusedView());

  // Tab to the settings button.
  PressAndReleaseKey(ui::VKEY_TAB);
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                SplitViewSetupView::kSettingsButtonIDForTest),
            focus_cycler->GetOverviewFocusedView());

  // Note we use `PressKeyAndModifierKeys()` to send modifier and key separately
  // to simulate real user input.

  // Shift + Tab reverse tabs to the dismiss button.
  auto* event_generator = GetEventGenerator();
  event_generator->PressKeyAndModifierKeys(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                SplitViewSetupView::kDismissButtonIDForTest),
            focus_cycler->GetOverviewFocusedView());

  // Shift + Tab reverse tabs to the overview item.
  event_generator->PressKeyAndModifierKeys(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_EQ(overview_windows[0]->item_widget(),
            focus_cycler->GetOverviewFocusedView()->GetWidget());
}

// Tests no crash when the faster splitview toast is destroyed. Regression test
// for http://b/336289329.
TEST_F(FasterSplitScreenTest, NoCrashOnToastDestroying) {
  auto w1 = CreateAppWindow(gfx::Rect(100, 100));
  auto w2 = CreateAppWindow(gfx::Rect(100, 100));

  // Snap `w1` to start faster splitview.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_TRUE(IsInOverviewSession());
  OverviewGrid* grid = GetOverviewSession()->grid_list()[0].get();
  auto* split_view_setup_widget = grid->split_view_setup_widget();
  ASSERT_TRUE(split_view_setup_widget);

  // Tab to the dismiss button.
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, GetEventGenerator());
  PressAndReleaseKey(ui::VKEY_TAB);
  OverviewFocusCycler* focus_cycler = GetOverviewSession()->focus_cycler();
  EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                SplitViewSetupView::kDismissButtonIDForTest),
            focus_cycler->GetOverviewFocusedView());

  // Enter tablet mode to destroy the toast.
  SwitchToTabletMode();

  // Exit tablet mode, then tab.
  ExitTabletMode();
  PressAndReleaseKey(ui::VKEY_TAB);
}

// Tests that the chromevox keys work as expected.
TEST_F(FasterSplitScreenTest, TabbingChromevox) {
  Shell::Get()->accessibility_controller()->spoken_feedback().SetEnabled(true);

  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  std::unique_ptr<aura::Window> window1(CreateAppWindow());

  const WindowSnapWMEvent snap_event(WM_EVENT_SNAP_PRIMARY,
                                     WindowSnapActionSource::kTest);

  enum class TestCase { kDismissButton, kSettingsButton };
  for (auto test_case : {TestCase::kDismissButton}) {
    WindowState::Get(window1.get())->OnWMEvent(&snap_event);
    ASSERT_TRUE(OverviewController::Get()->InOverviewSession());

    // Search + Right moves to the first overview item.
    PressAndReleaseKey(ui::VKEY_RIGHT, ui::EF_COMMAND_DOWN);
    const std::vector<std::unique_ptr<OverviewItemBase>>& overview_windows =
        GetOverviewItemsForRoot(0);
    OverviewGrid* grid = GetOverviewSession()->grid_list()[0].get();
    OverviewFocusCycler* focus_cycler = GetOverviewSession()->focus_cycler();
    EXPECT_EQ(overview_windows[0]->item_widget(),
              focus_cycler->GetOverviewFocusedView()->GetWidget());

    // Search + Right moves to the dismiss button.
    PressAndReleaseKey(ui::VKEY_RIGHT, ui::EF_COMMAND_DOWN);
    EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                  SplitViewSetupView::kDismissButtonIDForTest),
              focus_cycler->GetOverviewFocusedView());

    // Search + Right moves to the settings button.
    PressAndReleaseKey(ui::VKEY_RIGHT, ui::EF_COMMAND_DOWN);
    EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                  SplitViewSetupView::kSettingsButtonIDForTest),
              focus_cycler->GetOverviewFocusedView());

    switch (test_case) {
      case TestCase::kDismissButton: {
        // Search + Left moves back to the dismiss button.
        PressAndReleaseKey(ui::VKEY_LEFT, ui::EF_COMMAND_DOWN);
        EXPECT_EQ(grid->GetSplitViewSetupView()->GetViewByID(
                      SplitViewSetupView::kDismissButtonIDForTest),
                  focus_cycler->GetOverviewFocusedView());

        // Search + Space activates the dismiss button.
        PressAndReleaseKey(ui::VKEY_SPACE, ui::EF_COMMAND_DOWN);
        EXPECT_FALSE(IsInOverviewSession());
        break;
      }
      case TestCase::kSettingsButton: {
        // Search + Space activates the settings button.
        PressAndReleaseKey(ui::VKEY_SPACE, ui::EF_COMMAND_DOWN);
        EXPECT_FALSE(IsInOverviewSession());
        break;
      }
    }
  }
}

TEST_F(FasterSplitScreenTest, AccessibilityFocusAnnotator) {
  auto window1 = CreateAppWindow(gfx::Rect(100, 100));
  auto window0 = CreateAppWindow(gfx::Rect(100, 100));

  // Snap `window0`, so it is excluded from the overview list.
  SnapOneTestWindow(window0.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);

  auto* focus_widget = views::Widget::GetWidgetForNativeWindow(
      GetOverviewSession()->GetOverviewFocusWindow());
  ASSERT_TRUE(focus_widget);
  OverviewGrid* grid = GetOverviewSession()->grid_list()[0].get();
  ASSERT_FALSE(grid->desks_widget());
  ASSERT_FALSE(grid->GetSaveDeskForLaterButton());
  auto* split_view_setup_widget = grid->split_view_setup_widget();
  ASSERT_TRUE(split_view_setup_widget);

  // Overview items are in MRU order, so the expected order in the grid list is
  // the reverse creation order.
  auto* item_widget1 = GetOverviewItemForWindow(window1.get())->item_widget();

  // Order should be [focus_widget, item_widget1, split_view_setup_widget].
  CheckA11yOverrides("focus", focus_widget,
                     /*expected_previous=*/split_view_setup_widget,
                     /*expected_next=*/item_widget1);
  CheckA11yOverrides("item1", item_widget1, /*expected_previous=*/focus_widget,
                     /*expected_next=*/split_view_setup_widget);
  CheckA11yOverrides("splitview", split_view_setup_widget,
                     /*expected_previous=*/item_widget1,
                     /*expected_next=*/focus_widget);
}

// Tests if only the `kResizeBehaviorKey` is set, snapping the window does not
// start partial overview.
TEST_F(FasterSplitScreenTest, SnapUnresizableWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  w1->SetProperty(aura::client::kResizeBehaviorKey,
                  aura::client::kResizeBehaviorNone);

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifyNotSplitViewOrOverviewSession(w1.get());
}

// Tests if both the `kResizeBehaviorKey` and `kUnresizableSnappedSizeKey` are
// set, snapping the window starts partial overview.
TEST_F(FasterSplitScreenTest, SnapUnresizableCanSnapWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  w1->SetProperty(aura::client::kResizeBehaviorKey,
                  aura::client::kResizeBehaviorNone);
  w1->SetProperty(kUnresizableSnappedSizeKey, new gfx::Size(300, 0));

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
}

// Tests the histograms for the split view overview session exit points are
// recorded correctly in clamshell.
TEST_F(FasterSplitScreenTest,
       SplitViewOverviewSessionExitPointClamshellHistograms) {
  const auto kWindowLayoutCompleteOnSessionExit =
      BuildWindowLayoutCompleteOnSessionExitHistogram();
  const auto kSplitViewOverviewSessionExitPoint =
      BuildSplitViewOverviewExitPointHistogramName(
          WindowSnapActionSource::kDragWindowToEdgeToSnap);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // Verify the initial count for the histogram.
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/0);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/0);

  // Set up the splitview overview session and select a window in the partial
  // overview to complete the window layout.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, w2.get());
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/0);
  histogram_tester_.ExpectBucketCount(
      kSplitViewOverviewSessionExitPoint,
      SplitViewOverviewSessionExitPoint::kCompleteByActivating,
      /*expected_count=*/1);
  MaximizeToClearTheSession(w1.get());
  MaximizeToClearTheSession(w2.get());

  // Set up the splitview overview session and click an empty area to skip the
  // pairing.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
  auto* item2 = GetOverviewItemForWindow(w2.get());
  gfx::Point outside_point =
      gfx::ToRoundedPoint(item2->target_bounds().bottom_right());
  outside_point.Offset(/*delta_x=*/5, /*delta_y=*/5);
  event_generator->MoveMouseTo(outside_point);
  event_generator->ClickLeftButton();
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kSplitViewOverviewSessionExitPoint,
                                      SplitViewOverviewSessionExitPoint::kSkip,
                                      /*expected_count=*/1);
  MaximizeToClearTheSession(w1.get());
  MaximizeToClearTheSession(w2.get());

  // Set up the splitview overview session, create a 3rd window to be
  // auto-snapped and complete the window layout.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/2);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(
      kSplitViewOverviewSessionExitPoint,
      SplitViewOverviewSessionExitPoint::kCompleteByActivating,
      /*expected_count=*/2);
  MaximizeToClearTheSession(w1.get());
  MaximizeToClearTheSession(w3.get());

  // Set up the splitview overview session and press escape key to skip pairing.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
  event_generator->PressAndReleaseKey(ui::VKEY_ESCAPE);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/2);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/2);
  histogram_tester_.ExpectBucketCount(kSplitViewOverviewSessionExitPoint,
                                      SplitViewOverviewSessionExitPoint::kSkip,
                                      /*expected_count=*/2);
  MaximizeToClearTheSession(w1.get());
  MaximizeToClearTheSession(w2.get());

  // Set up the splitview overview session and close the snapped window to exit
  // the session.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(w1.get());
  w1.reset();
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/true,
                                      /*expected_count=*/2);
  histogram_tester_.ExpectBucketCount(kWindowLayoutCompleteOnSessionExit,
                                      /*sample=*/false,
                                      /*expected_count=*/2);
  histogram_tester_.ExpectBucketCount(
      kSplitViewOverviewSessionExitPoint,
      SplitViewOverviewSessionExitPoint::kWindowDestroy,
      /*expected_count=*/1);
}

// Integration test of the `SplitViewOverviewSession` exit point with drag to
// snap action source. Verify that the end-to-end metric is recorded correctly.
TEST_F(FasterSplitScreenTest, KeyMetricsIntegrationTest_DragToSnap) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  const auto kSplitViewOverviewSessionExitPoint =
      BuildSplitViewOverviewExitPointHistogramName(
          WindowSnapActionSource::kDragWindowToEdgeToSnap);
  histogram_tester_.ExpectBucketCount(
      kSplitViewOverviewSessionExitPoint,
      SplitViewOverviewSessionExitPoint::kCompleteByActivating,
      /*expected_count=*/0);

  // Drag a window to snap on the primary snapped position and verify the
  // metrics.
  std::unique_ptr<WindowResizer> resizer(CreateWindowResizer(
      w1.get(), gfx::PointF(), HTCAPTION, wm::WINDOW_MOVE_SOURCE_MOUSE));
  resizer->Drag(gfx::PointF(0, 400), /*event_flags=*/0);
  resizer->CompleteDrag();
  resizer.reset();
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_EQ(
      GetSplitViewOverviewSession(w1.get())->snap_action_source_for_testing(),
      WindowSnapActionSource::kDragWindowToEdgeToSnap);
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, w2.get());
  histogram_tester_.ExpectBucketCount(
      kSplitViewOverviewSessionExitPoint,
      SplitViewOverviewSessionExitPoint::kCompleteByActivating,
      /*expected_count=*/1);

  MaximizeToClearTheSession(w1.get());
  MaximizeToClearTheSession(w2.get());

  // Drag a window to snap on the secondary snapped position and verify the
  // metrics.
  resizer = CreateWindowResizer(w1.get(), gfx::PointF(), HTCAPTION,
                                wm::WINDOW_MOVE_SOURCE_MOUSE);
  resizer->Drag(gfx::PointF(800, 0), /*event_flags=*/0);
  resizer->CompleteDrag();
  resizer.reset();
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_EQ(
      GetSplitViewOverviewSession(w1.get())->snap_action_source_for_testing(),
      WindowSnapActionSource::kDragWindowToEdgeToSnap);

  auto* item2 = GetOverviewItemForWindow(w2.get());
  gfx::Point outside_point =
      gfx::ToRoundedPoint(item2->target_bounds().bottom_right());
  outside_point.Offset(/*delta_x=*/5, /*delta_y=*/5);
  event_generator->MoveMouseTo(outside_point);
  event_generator->ClickLeftButton();
  histogram_tester_.ExpectBucketCount(kSplitViewOverviewSessionExitPoint,
                                      SplitViewOverviewSessionExitPoint::kSkip,
                                      /*expected_count=*/1);
  MaximizeToClearTheSession(w1.get());
}

// Integration test of the `SplitViewOverviewSession` exit point with window
// size button as the snap action source. Verify that the end-to-end metric is
// recorded correctly.
TEST_F(FasterSplitScreenTest, KeyMetricsIntegrationTest_WindowSizeButton) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  struct SnapRequestWithActionSource {
    chromeos::SnapController::SnapRequestSource request_source;
    WindowSnapActionSource snap_action_source;
  } kTestCases[]{
      {chromeos::SnapController::SnapRequestSource::kWindowLayoutMenu,
       WindowSnapActionSource::kSnapByWindowLayoutMenu},
      {chromeos::SnapController::SnapRequestSource::kSnapButton,
       WindowSnapActionSource::kLongPressCaptionButtonToSnap},
  };

  for (const auto test_case : kTestCases) {
    const auto kSplitViewOverviewSessionExitPoint =
        BuildSplitViewOverviewExitPointHistogramName(
            test_case.snap_action_source);
    histogram_tester_.ExpectBucketCount(
        kSplitViewOverviewSessionExitPoint,
        SplitViewOverviewSessionExitPoint::kCompleteByActivating,
        /*expected_count=*/0);

    auto commit_snap = [&]() {
      chromeos::SnapController::Get()->CommitSnap(
          w1.get(), chromeos::SnapDirection::kSecondary,
          chromeos::kDefaultSnapRatio, test_case.request_source);
          VerifySplitViewOverviewSession(w1.get());
          EXPECT_EQ(GetSplitViewOverviewSession(w1.get())
                        ->snap_action_source_for_testing(),
                    test_case.snap_action_source);
    };

    commit_snap();
    auto* event_generator = GetEventGenerator();
    ClickOverviewItem(GetEventGenerator(), w2.get());
    histogram_tester_.ExpectBucketCount(
        kSplitViewOverviewSessionExitPoint,
        SplitViewOverviewSessionExitPoint::kCompleteByActivating,
        /*expected_count=*/1);
    MaximizeToClearTheSession(w1.get());
    MaximizeToClearTheSession(w2.get());

    commit_snap();
    auto* item2 = GetOverviewItemForWindow(w2.get());
    gfx::Point outside_point =
        gfx::ToRoundedPoint(item2->target_bounds().bottom_right());
    outside_point.Offset(/*delta_x=*/5, /*delta_y=*/5);
    event_generator->MoveMouseTo(outside_point);
    event_generator->ClickLeftButton();

    histogram_tester_.ExpectBucketCount(
        kSplitViewOverviewSessionExitPoint,
        SplitViewOverviewSessionExitPoint::kSkip,
        /*expected_count=*/1);
    MaximizeToClearTheSession(w1.get());
  }
}

// Tests that the `OverviewStartAction` will be recorded correctly in uma for
// the faster split screen setup.
TEST_F(FasterSplitScreenTest, OverviewStartActionHistogramTest) {
  constexpr char kOverviewStartActionHistogram[] = "Ash.Overview.StartAction";
  // Verify the initial count for the histogram.
  histogram_tester_.ExpectBucketCount(
      kOverviewStartActionHistogram,
      OverviewStartAction::kFasterSplitScreenSetup,
      /*expected_count=*/0);
  std::unique_ptr<aura::Window> window1(CreateAppWindow());
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(window1.get());
  histogram_tester_.ExpectBucketCount(
      kOverviewStartActionHistogram,
      OverviewStartAction::kFasterSplitScreenSetup,
      /*expected_count=*/1);
}

// Tests that a11y alert will be announced upon entering the faster split screen
// setup session.
TEST_F(FasterSplitScreenTest, A11yAlertOnEnteringFaterSplitScreenSetup) {
  TestAccessibilityControllerClient client;
  std::unique_ptr<aura::Window> window1(CreateAppWindow());
  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  EXPECT_NE(AccessibilityAlert::FASTER_SPLIT_SCREEN_SETUP,
            client.last_a11y_alert());
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_EQ(AccessibilityAlert::FASTER_SPLIT_SCREEN_SETUP,
            client.last_a11y_alert());
}

// Tests that there will be no crash when dragging a snapped window in overview
// toward the edge. In this case, the overview components will become too small
// to meet the minimum requirement of the fundamental UI layer such as shadow.
// See the regression behavior in http://b/324478757.
TEST_F(FasterSplitScreenTest, NoCrashWhenDraggingSnappedWindowToEdge) {
  std::unique_ptr<aura::Window> window1(
      CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> window2(
      CreateAppWindow(gfx::Rect(100, 100, 200, 100)));
  SnapOneTestWindow(window1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  WaitForOverviewEntered();
  VerifySplitViewOverviewSession(window1.get());

  // Drag the snapped window towards the edge of the work area and verify that
  // there is no crash.
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(
      window1.get()->GetBoundsInScreen().right_center());
  gfx::Point drag_end_point = GetWorkAreaBounds().right_center();
  drag_end_point.Offset(/*delta_x=*/-10, 0);
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(drag_end_point);

  // Verify that shadow exists for overview item.
  auto* overview_item2 = GetOverviewItemForWindow(window2.get());
  const auto shadow_content_bounds =
      overview_item2->get_shadow_content_bounds_for_testing();
  EXPECT_FALSE(shadow_content_bounds.IsEmpty());

  VerifySplitViewOverviewSession(window1.get());
  EXPECT_TRUE(WindowState::Get(window1.get())->is_dragged());
}

TEST_F(FasterSplitScreenTest, RecordWindowIndexAndCount) {
  // Start partial overview with 1 window in overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  // Select `w2` which is the only window.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  histogram_tester_.ExpectBucketCount(kPartialOverviewSelectedWindowIndex,
                                      /*sample=*/0,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kPartialOverviewWindowListSize,
                                      /*sample=*/1, /*expected_count=*/1);
  MaximizeToClearTheSession(w2.get());

  // Start partial overview with 2 windows in overview.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  // Select `w2` which is the 2nd mru window.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  histogram_tester_.ExpectBucketCount(kPartialOverviewSelectedWindowIndex,
                                      /*sample=*/1,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kPartialOverviewWindowListSize,
                                      /*sample=*/2, /*expected_count=*/1);
  MaximizeToClearTheSession(w2.get());

  // Start partial overview with 3 windows in overview.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  // Select `w3` which is the 3rd mru window.
  ClickOverviewItem(GetEventGenerator(), w3.get());
  histogram_tester_.ExpectBucketCount(kPartialOverviewSelectedWindowIndex,
                                      /*sample=*/2,
                                      /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(kPartialOverviewWindowListSize,
                                      /*sample=*/3, /*expected_count=*/1);
}

// -----------------------------------------------------------------------------
// SnapGroupDisabledTest:

class SnapGroupDisabledTest : public AshTestBase {
 public:
  SnapGroupDisabledTest() {
    scoped_feature_list_.InitWithFeatures(/*enabled_features=*/
                                          {},
                                          /*disabled_features=*/{
                                              features::kSnapGroup});
  }
  SnapGroupDisabledTest(const SnapGroupDisabledTest&) = delete;
  SnapGroupDisabledTest& operator=(const SnapGroupDisabledTest&) = delete;
  ~SnapGroupDisabledTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that faster splitscreen still works with `SnapGroup` disabled.
TEST_F(SnapGroupDisabledTest, FasterSplitScreenSetup) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(w1.get());
}

// -----------------------------------------------------------------------------
// SnapGroupTest:

// A test fixture to test the snap group feature.
class SnapGroupTest : public SnapGroupTestBase {
 public:
  template <typename... TaskEnvironmentTraits>
  explicit SnapGroupTest(TaskEnvironmentTraits&&... traits)
      : SnapGroupTestBase(std::forward<TaskEnvironmentTraits>(traits)...) {
    scoped_feature_list_
        .InitWithFeatures(/*enabled_features=*/
                          {features::kSnapGroup,
                           features::kOsSettingsRevampWayfinding,
                           features::kSameAppWindowCycle},
                          /*disabled_features=*/{});
  }
  SnapGroupTest(const SnapGroupTest&) = delete;
  SnapGroupTest& operator=(const SnapGroupTest&) = delete;
  ~SnapGroupTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kUseFirstDisplayAsInternal);
  }

  void CompleteWindowCycling() {
    WindowCycleController* window_cycle_controller =
        Shell::Get()->window_cycle_controller();
    window_cycle_controller->CompleteCycling();
    EXPECT_FALSE(window_cycle_controller->IsCycling());
  }

  void CycleWindow(WindowCyclingDirection direction, int steps) {
    WindowCycleController* window_cycle_controller =
        Shell::Get()->window_cycle_controller();
    for (int i = 0; i < steps; i++) {
      window_cycle_controller->HandleCycleWindow(direction);
      EXPECT_TRUE(window_cycle_controller->IsCycling());
    }
  }

  // TODO(michelefan): Consider put this test util in a base class or test file.
  std::unique_ptr<aura::Window> CreateTestWindowWithAppID(
      std::string app_id_key) {
    std::unique_ptr<aura::Window> window = CreateAppWindow();
    window->SetProperty(kAppIDKey, std::move(app_id_key));
    return window;
  }

  std::unique_ptr<aura::Window> CreateTransientChildWindow(
      aura::Window* transient_parent,
      gfx::Rect child_window_bounds) {
    auto child = CreateAppWindow(child_window_bounds);
    wm::AddTransientChild(transient_parent, child.get());
    return child;
  }

  std::unique_ptr<aura::Window> CreateAlwaysOnTopWindow() {
    std::unique_ptr<aura::Window> always_on_top_window(CreateAppWindow());
    always_on_top_window->SetProperty(aura::client::kZOrderingKey,
                                      ui::ZOrderLevel::kFloatingWindow);
    EXPECT_EQ(kShellWindowId_AlwaysOnTopContainer,
              always_on_top_window->parent()->GetId());
    return always_on_top_window;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that if the user disables the pref for snap window suggestions, we
// still add snap group.
TEST_F(SnapGroupTest, DisableSnapWindowSuggestionsPref) {
  PrefService* pref =
      Shell::Get()->session_controller()->GetActivePrefService();

  pref->SetBoolean(prefs::kSnapWindowSuggestions, false);
  ASSERT_FALSE(pref->GetBoolean(prefs::kSnapWindowSuggestions));

  // Snap a window. Test we don't start overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifyNotSplitViewOrOverviewSession(w1.get());

  // Turn on the pref. Test we start overview.
  pref->SetBoolean(prefs::kSnapWindowSuggestions, true);
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(w1.get());

  // Select `w2` from overview. Test we form a group.
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that the creation and removal of snap group.
TEST_F(SnapGroupTest, AddAndRemoveSnapGroupTest) {
  auto* snap_group_controller = SnapGroupController::Get();
  const auto& snap_groups = snap_group_controller->snap_groups_for_testing();
  const auto& window_to_snap_group_map =
      snap_group_controller->window_to_snap_group_map_for_testing();
  EXPECT_EQ(snap_groups.size(), 0u);
  EXPECT_EQ(window_to_snap_group_map.size(), 0u);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_FALSE(snap_group_controller->AddSnapGroup(
      w1.get(), w3.get(), /*replace=*/false,
      /*carry_over_creation_Time=*/std::nullopt));

  EXPECT_EQ(snap_groups.size(), 1u);
  EXPECT_EQ(window_to_snap_group_map.size(), 2u);
  const auto iter1 = window_to_snap_group_map.find(w1.get());
  ASSERT_TRUE(iter1 != window_to_snap_group_map.end());
  const auto iter2 = window_to_snap_group_map.find(w2.get());
  ASSERT_TRUE(iter2 != window_to_snap_group_map.end());
  auto* snap_group = snap_groups.back().get();
  EXPECT_EQ(iter1->second, snap_group);
  EXPECT_EQ(iter2->second, snap_group);

  ASSERT_TRUE(snap_group_controller->RemoveSnapGroup(
      snap_group, SnapGroupExitPoint::kDragWindowOut));
  ASSERT_TRUE(snap_groups.empty());
  ASSERT_TRUE(window_to_snap_group_map.empty());
}

// Verify that when there is no gap between the edges of the windows and the
// divider in a Snap Group in landscape. See the regression at
// http://b/333618907.
TEST_F(SnapGroupTest, NoGapAfterSnapGroupCreationInLandscape) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  UpdateDisplay("1366x768");

  // The minimum size of the window is set to a value between 1/3 and 1/2 of the
  // work area's width to simulate the browser window CUJ.
  const gfx::Size window_minimum_size = gfx::Size(500, 0);

  aura::test::TestWindowDelegate delegate1;
  std::unique_ptr<aura::Window> w1(CreateTestWindowInShellWithDelegate(
      &delegate1, /*id=*/-1, gfx::Rect(800, 600)));
  delegate1.set_minimum_size(window_minimum_size);
  w1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  aura::test::TestWindowDelegate delegate2;
  std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
      &delegate2, /*id=*/-1, gfx::Rect(500, 0, 800, 600)));
  delegate2.set_minimum_size(window_minimum_size);
  w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  WaitForOverviewEntered();
  VerifySplitViewOverviewSession(w1.get());
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  WaitForOverviewExitAnimation();

  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Verify that when there is no gap between the edges of the windows and the
// divider in a Snap Group in portrait. See the regression at
// http://b/335323278.
TEST_F(SnapGroupTest, NoGapAfterSnapGroupCreationInPortrait) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  UpdateDisplay("768x1366");

  // The minimum size of the window is set to a value between 1/3 and 1/2 of the
  // work area's height to simulate the browser window CUJ.
  const gfx::Size window_minimum_size = gfx::Size(0, 500);

  aura::test::TestWindowDelegate delegate1;
  std::unique_ptr<aura::Window> w1(CreateTestWindowInShellWithDelegate(
      &delegate1, /*id=*/-1, gfx::Rect(800, 600)));
  w1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  delegate1.set_minimum_size(window_minimum_size);
  aura::test::TestWindowDelegate delegate2;
  std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
      &delegate2, /*id=*/-1, gfx::Rect(500, 0, 800, 600)));
  delegate2.set_minimum_size(window_minimum_size);
  w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  WaitForOverviewEntered();
  VerifySplitViewOverviewSession(w1.get());
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  WaitForOverviewExitAnimation();

  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Verify snap group will not be formed when attempting to include a window from
// the always-on-top container.
TEST_F(SnapGroupTest, DisallowFormSnapGroupWithAlwaysOnTopWindow) {
  std::unique_ptr<aura::Window> normal_window(CreateAppWindow());
  std::unique_ptr<aura::Window> always_on_top_window(CreateAlwaysOnTopWindow());

  SnapOneTestWindow(normal_window.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(normal_window.get());
  OverviewItemBase* always_on_top_overview_item =
      GetOverviewItemForWindow(always_on_top_window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(gfx::ToRoundedPoint(
      always_on_top_overview_item->target_bounds().CenterPoint()));
  event_generator->ClickLeftButton();

  // Verify that Snap Group will not be formed after activating
  // `always_on_top_window` in partial Overview.
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(
      normal_window.get(), always_on_top_window.get()));
  EXPECT_FALSE(
      snap_group_controller->GetSnapGroupForGivenWindow(normal_window.get()));
  EXPECT_FALSE(
      snap_group_controller->GetSnapGroupForGivenWindow(normal_window.get()));
}

// Verify that a window configured to be "visible on all workspaces" cannot be
// part of any Snap Group.
TEST_F(SnapGroupTest, DisallowVisibleOnAllWorkspacesWindowToFormGroup) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  auto* window_widget0 = views::Widget::GetWidgetForNativeView(w0.get());
  // Configure the property for `w0` to be visible on all workspaces.
  window_widget0->SetVisibleOnAllWorkspaces(true);
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  SnapOneTestWindow(w0.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w0.get());
  OverviewItemBase* overview_item1 = GetOverviewItemForWindow(w1.get());
  const gfx::Point overview_item1_center =
      gfx::ToRoundedPoint(overview_item1->target_bounds().CenterPoint());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(overview_item1_center);
  event_generator->ClickLeftButton();

  // Verify that Snap Group will not be formed after activating `w1` in partial
  // Overview.
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w0.get(), w1.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w0.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
}

// Tests that the shelf's corners are rounded by default. Upon Snap Group
// creation, the shelf's corners become sharp. When the Snap Group breaks, the
// shelf returns to its default state with rounded corners.
TEST_F(SnapGroupTest, ShelfRoundedCornersInFasterSplitScreenEntryPoint) {
  ShelfLayoutManager* shelf_layout_manager =
      AshTestBase::GetPrimaryShelf()->shelf_layout_manager();
  ASSERT_EQ(ShelfBackgroundType::kDefaultBg,
            shelf_layout_manager->shelf_background_type());

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Test that Shelf will be updated to have sharp rounded corners.
  EXPECT_EQ(ShelfBackgroundType::kMaximized,
            shelf_layout_manager->shelf_background_type());

  // Drag `w1` out to break the group.
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().top_center());
  aura::test::TestWindowDelegate().set_window_component(HTCAPTION);
  event_generator->PressLeftButton();
  event_generator->MoveMouseBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w1.get())->is_dragged());
  event_generator->ReleaseLeftButton();
  ASSERT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Verify that Shelf restores its default background type with rounded
  // corners.
  EXPECT_EQ(ShelfBackgroundType::kDefaultBg,
            shelf_layout_manager->shelf_background_type());
}

// Test that dragging a snapped window's caption hides the divider and that the
// snap group will be removed on drag complete.
TEST_F(SnapGroupTest, DragSnappedWindowExitPointTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  aura::test::TestWindowDelegate test_window_delegate;

  // Test dragging a snapped window out by mouse to exit the group.
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().top_center());
  test_window_delegate.set_window_component(HTCAPTION);
  event_generator->PressLeftButton();
  event_generator->MoveMouseBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w1.get())->is_dragged());
  EXPECT_FALSE(GetTopmostSnapGroupDivider());

  event_generator->ReleaseLeftButton();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  MaximizeToClearTheSession(w2.get());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Test dragging a snapped window out by touch to exit the group.
  event_generator->MoveTouch(w1->GetBoundsInScreen().top_center());
  test_window_delegate.set_window_component(HTCAPTION);
  event_generator->PressTouch();
  event_generator->MoveTouchBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w1.get())->is_dragged());
  EXPECT_FALSE(GetTopmostSnapGroupDivider());

  event_generator->ReleaseTouch();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that dragging a window within a Snap Group to the same snap position
// will break the existing Snap Group. See regression at http://b/335311879.
TEST_F(SnapGroupTest, DragSnappedWindowToSnapWithDifferentSnapRatio) {
  UpdateDisplay("1200x900");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  ASSERT_TRUE(IsInOverviewSession());
  ClickOverviewItem(GetEventGenerator(), w2.get());

  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  aura::test::TestWindowDelegate test_window_delegate;

  // Drag a snapped window out by mouse to exit the group.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(w2->GetBoundsInScreen().top_center());
  test_window_delegate.set_window_component(HTCAPTION);
  event_generator->PressLeftButton();
  event_generator->MoveMouseBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w2.get())->is_dragged());
  // The existing Snap Group will break.
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Drag to re-snap `w2` to the same snap position but with
  // `chromeos::kDefaultSnapRatio`.
  event_generator->MoveMouseTo(gfx::Point(1250, 0));
  event_generator->ReleaseLeftButton();
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());

  // Two windows remain not in a Snap Group.
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that when snapping the snapped window to the opposite side, partial
// overview will be triggered and that the snap group will be removed.
TEST_F(SnapGroupTest, SnapToTheOppositeSideToExit) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Snap the current primary window as the secondary window, partial overview
  // will be triggered.
  SnapOneTestWindow(w1.get(),
                    /*state_type=*/WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Select the other window in overview to re-form a snap group.
  ClickOverviewItem(event_generator, w2.get());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests to verify that dragging a window out of a snap group breaks the group
// and removes the divider.
TEST_F(SnapGroupTest, DragWindowOutToBreakSnapGroup) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  // Create `w2` with `HTCAPTION`. This ensures that dragging behavior is
  // initiated from the caption region. `SnapGroup::OnLocatedEvent()` will
  // process of this event.
  aura::test::TestWindowDelegate test_window_delegate;
  test_window_delegate.set_window_component(HTCAPTION);
  std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
      &test_window_delegate, aura::client::WINDOW_TYPE_NORMAL,
      gfx::Rect(400, 5, 100, 50)));
  w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);

  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());

  event_generator->set_current_screen_location(
      w2->GetBoundsInScreen().top_center());
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
  EXPECT_FALSE(GetTopmostSnapGroupDivider());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// This class simulates a crash scenario that can occur within the
// `ToplevelWindowEventHandler`. Specifically, when a window belonging to a Snap
// Group is dragged out to break the group, the `window_resizer_` can be reset,
// leading to a crash.
class ToplevelWindowEventHandlerCrashSimulator : public SnapGroupObserver {
 public:
  ToplevelWindowEventHandlerCrashSimulator() {
    SnapGroupController::Get()->AddObserver(this);
  }
  ToplevelWindowEventHandlerCrashSimulator(
      const ToplevelWindowEventHandlerCrashSimulator&) = delete;
  ToplevelWindowEventHandlerCrashSimulator& operator=(
      const ToplevelWindowEventHandlerCrashSimulator&) = delete;
  ~ToplevelWindowEventHandlerCrashSimulator() override {
    SnapGroupController::Get()->RemoveObserver(this);
  }

  // SnapGroupObserver:
  void OnSnapGroupRemoving(SnapGroup* snap_group,
                           SnapGroupExitPoint exit_pint) override {
    if (exit_pint != SnapGroupExitPoint::kDragWindowOut) {
      return;
    }

    // Explicitly reset the `window_resizier_` on window drag out from a Snap
    // Group to simulate the scenario that may possibly lead to crash, reported
    // in http://b/348673912.
    ToplevelWindowEventHandler* toplevel_window_event_handler =
        Shell::Get()->toplevel_window_event_handler();
    toplevel_window_event_handler->ResetWindowResizerForTesting();
  }
};

// Tests that dragging a window out of a Snap Group does not cause a crash,
// even if the window_resizer_ is reset during the process. Regression test for
// http://b/348673912.
TEST_F(SnapGroupTest, ToplevelWindowEventHandlerDragCrashFix) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  aura::test::TestWindowDelegate test_window_delegate;
  test_window_delegate.set_window_component(HTCAPTION);
  std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
      &test_window_delegate, aura::client::WINDOW_TYPE_NORMAL,
      gfx::Rect(400, 5, 100, 50)));
  w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);

  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());

  ToplevelWindowEventHandlerCrashSimulator toplevel_window_drag_simulator;

  event_generator->set_current_screen_location(
      w2->GetBoundsInScreen().top_center());
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());

  EXPECT_FALSE(GetTopmostSnapGroupDivider());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// This class simulates a crash scenario that can occur in
// `SplitViewOverviewSession::OnWindowBoundsChanged()`.
class OverviewCrashSimulator : public OverviewObserver {
 public:
  OverviewCrashSimulator() { OverviewController::Get()->AddObserver(this); }
  OverviewCrashSimulator(const OverviewCrashSimulator&) = delete;
  OverviewCrashSimulator& operator=(const OverviewCrashSimulator&) = delete;
  ~OverviewCrashSimulator() override {
    OverviewController::Get()->RemoveObserver(this);
  }

  // OverviewObserver:
  void OnOverviewModeEnding(OverviewSession* overview_session) override {
    auto* split_view_overview_session =
        GetSplitViewOverviewSession(Shell::GetPrimaryRootWindow());
    if (!split_view_overview_session) {
      return;
    }
    // The crash can occur in any scenario where ending overview would cause a
    // window bounds animation and notify
    // `SplitViewOverviewSession::OnWindowBoundsChanged()`. Send a bounds events
    // to `window` with animation.
    aura::Window* window = split_view_overview_session->window();
    const SetBoundsWMEvent event(gfx::Rect(100, 100), /*animate=*/true);
    WindowState::Get(window)->OnWMEvent(&event);
  }
};

// Tests no crash when ending overview causes a window animation. Regression
// test for http://b/352383998.
TEST_F(SnapGroupTest, NoCrashOnOverviewModeEnding) {
  OverviewCrashSimulator overview_crash_simulator;

  // Start partial overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // End overview.
  OverviewController::Get()->EndOverview(OverviewEndAction::kTests);
}

// Test that maximizing a snapped window breaks the snap group.
TEST_F(SnapGroupTest, MaximizeSnappedWindowExitPointTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  WindowState::Get(w2.get())->Maximize();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that the corresponding snap group will be removed when one of the
// windows in the snap group gets destroyed.
TEST_F(SnapGroupTest, WindowDestroyToBreakSnapGroup) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const auto& snap_groups = snap_group_controller->snap_groups_for_testing();
  const auto& window_to_snap_group_map =
      snap_group_controller->window_to_snap_group_map_for_testing();
  EXPECT_EQ(snap_groups.size(), 1u);
  EXPECT_EQ(window_to_snap_group_map.size(), 2u);

  // Destroy one window in the snap group and the entire snap group will be
  // removed.
  w1.reset();
  EXPECT_TRUE(snap_groups.empty());
  EXPECT_TRUE(window_to_snap_group_map.empty());
}

// Tests that if one window in the snap group is actiaved, the stacking order of
// the other window in the snap group will be updated to be right below the
// activated window i.e. the two windows in the snap group will be placed on
// top.
TEST_F(SnapGroupTest, WindowStackingOrderTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));

  wm::ActivateWindow(w3.get());

  // Actiave one of the windows in the snap group.
  wm::ActivateWindow(w1.get());

  MruWindowTracker::WindowList window_list =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
  EXPECT_EQ(window_list, aura::WindowTracker::WindowList({
                             w1.get(),
                             w3.get(),
                             w2.get(),
                         }));

  // `w3` is stacked below `w2` even though the activation order of `w3` is
  // before `w2`.
  EXPECT_TRUE(window_util::IsStackedBelow(w3.get(), w2.get()));
}

// Tests that when there is one snapped window and overview open, creating a new
// window, i.e. by clicking the shelf icon, will auto-snap it.
TEST_F(SnapGroupTest, AutoSnapNewWindow) {
  // Snap `w1` to start split view overview session.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(),
                    /*state_type=*/WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Create a new `w3`. Test it auto-snaps and forms a snap group with `w1`.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w3.get())->GetStateType());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w3.get()));
}

TEST_F(SnapGroupTest, DontAutoSnapNewWindowOutsideSplitViewOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_FALSE(
      RootWindowController::ForWindow(w1.get())->split_view_overview_session());
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());

  // Open a third window. Test it does *not* snap.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
  EXPECT_FALSE(WindowState::Get(w3.get())->IsSnapped());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
}

// Tests the snap ratio is updated correctly when resizing the windows in a snap
// group with the split view divider.
TEST_F(SnapGroupTest, SnapRatioTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  const gfx::Point hover_location =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  GetTopmostSnapGroupDivider()->StartResizeWithDivider(hover_location);
  const auto end_point =
      hover_location + gfx::Vector2d(-GetWorkAreaBounds().width() / 6, 0);
  GetTopmostSnapGroupDivider()->ResizeWithDivider(end_point);
  GetTopmostSnapGroupDivider()->EndResizeWithDivider(end_point);

  // Verify that split view remains inactive to avoid split view specific
  // behaviors such as auto-snap or showing cannot snap toast.
  EXPECT_FALSE(GetSplitViewController()->InSplitViewMode());
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              WindowState::Get(w1.get())->snap_ratio().value(),
              /*abs_error=*/0.1);
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              WindowState::Get(w2.get())->snap_ratio().value(),
              /*abs_error=*/0.1);
}

// Tests that the windows in a snap group can be resized to an arbitrary
// location with the split view divider if neither of the windows has the
// minimum size constraints.
TEST_F(SnapGroupTest, ResizeWithSplitViewDividerToArbitraryLocations) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  for (const auto& shelf_alignment :
       {ShelfAlignment::kBottom, ShelfAlignment::kLeft}) {
    std::stringstream ss;
    ss << shelf_alignment;
    SCOPED_TRACE("Shelf alignment = " + ss.str());
    GetPrimaryShelf()->SetAlignment(shelf_alignment);

    auto* event_generator = GetEventGenerator();
    const gfx::Point divider_center(
        GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
    event_generator->MoveMouseTo(divider_center);
    event_generator->PressLeftButton();

    for (const int resize_delta : {-10, 6, -15}) {
      const gfx::Point resize_point(divider_center +
                                    gfx::Vector2d(resize_delta, 0));
      event_generator->MoveMouseTo(resize_point, /*count=*/2);
      EXPECT_EQ(resize_point,
                GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
      UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                       GetTopmostSnapGroupDivider());
    }
    event_generator->ReleaseLeftButton();
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());
  }
}

// Tests that the divider resizing respects the window's minimum size
// constraints.
TEST_F(SnapGroupTest, RespectWindowMinimumSizeWhileResizingWithDivider) {
  UpdateDisplay("1200x900");

  std::unique_ptr<aura::Window> window1(
      CreateAppWindowWithMinSize(gfx::Size(300, 600)));

  std::unique_ptr<aura::Window> window2(CreateAppWindow());
  SnapTwoTestWindows(window1.get(), window2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // The divider position updates while dragging, if it doesn't go below the
  // window's minimum size.
  GetTopmostSnapGroupDivider()->StartResizeWithDivider(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  GetTopmostSnapGroupDivider()->ResizeWithDivider(gfx::Point(400, 200));
  EXPECT_GT(GetTopmostSnapGroupDivider()->divider_position(), 300);
  GetTopmostSnapGroupDivider()->EndResizeWithDivider(gfx::Point(400, 200));
  EXPECT_GT(GetTopmostSnapGroupDivider()->divider_position(), 300);

  // Attempt to drag the divider below the window's minimum size. Verify it
  // stops at the minimum.
  GetTopmostSnapGroupDivider()->StartResizeWithDivider(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  GetTopmostSnapGroupDivider()->ResizeWithDivider(gfx::Point(200, 200));
  EXPECT_EQ(GetTopmostSnapGroupDivider()->divider_position(), 300);
  GetTopmostSnapGroupDivider()->EndResizeWithDivider(gfx::Point(200, 200));
  EXPECT_EQ(GetTopmostSnapGroupDivider()->divider_position(), 300);
}

// Tests that a snap group and the split view divider will be will be
// automatically created on two windows snapped in the clamshell mode. The snap
// group will be removed together with the split view divider on destroying of
// one window in the snap group.
TEST_F(SnapGroupTest, AutomaticallyCreateGroupOnTwoWindowsSnappedInClamshell) {
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller);
  const auto& snap_groups = snap_group_controller->snap_groups_for_testing();
  const auto& window_to_snap_group_map =
      snap_group_controller->window_to_snap_group_map_for_testing();
  EXPECT_TRUE(snap_groups.empty());
  EXPECT_TRUE(window_to_snap_group_map.empty());

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_EQ(snap_groups.size(), 1u);
  EXPECT_EQ(window_to_snap_group_map.size(), 2u);

  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  wm::ActivateWindow(w2.get());
  EXPECT_TRUE(window_util::IsStackedBelow(w3.get(), w1.get()));

  w1.reset();
  EXPECT_FALSE(GetTopmostSnapGroupDivider());
  EXPECT_TRUE(snap_groups.empty());
  EXPECT_TRUE(window_to_snap_group_map.empty());
}

// Tests we correctly end split view if partial overview is skipped and another
// window is snapped. Regression test for http://b/333600706.
TEST_F(SnapGroupTest, EndSplitView) {
  // Snap `w1` to start partial overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  OverviewController* overview_controller = OverviewController::Get();
  EXPECT_TRUE(overview_controller->InOverviewSession());
  EXPECT_TRUE(GetSplitViewController()->primary_window());
  // Skip partial overview.
  PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  // Test we cleared the observed window.
  EXPECT_FALSE(GetSplitViewController()->primary_window());
  EXPECT_FALSE(overview_controller->InOverviewSession());

  // Drag to snap `w2` to the opposite side.
  wm::ActivateWindow(w2.get());
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().right_center());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  // Test we cleared the observed window.
  EXPECT_FALSE(GetSplitViewController()->secondary_window());
  EXPECT_FALSE(overview_controller->InOverviewSession());

  // Activate `w1`. Test we don't create a snap group.
  wm::ActivateWindow(w1.get());
}

// Tests that when the auto-snapped window has minimum size that doesn't fit the
// work area, we adjust the divider and window bounds.
TEST_F(SnapGroupTest, AutoSnapWindowWithMinimumSize) {
  // Test with both the bottom and side shelf.
  for (const auto& shelf_alignment :
       {ShelfAlignment::kBottom, ShelfAlignment::kLeft}) {
    std::stringstream ss;
    ss << shelf_alignment;
    SCOPED_TRACE("Shelf alignment = " + ss.str());
    GetPrimaryShelf()->SetAlignment(shelf_alignment);
    // Create `w2` so it doesn't fit on the other side of `w1`.
    std::unique_ptr<aura::Window> w1(CreateAppWindow());
    const gfx::Rect work_area(GetWorkAreaBounds());
    const int min_width = work_area.width() * 0.4f;
    std::unique_ptr<aura::Window> w2(
        CreateAppWindowWithMinSize(gfx::Size(min_width, work_area.height())));

    // Snap `w1` to start partial overview.
    SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                      chromeos::kTwoThirdSnapRatio);
    EXPECT_TRUE(OverviewController::Get()->InOverviewSession());

    // Select `w2` to be auto-snapped.
    ClickOverviewItem(GetEventGenerator(), w2.get());
    auto* snap_group_controller = SnapGroupController::Get();
    EXPECT_TRUE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

    // Expect `w2` is snapped at its minimum width and `w1` and the divider are
    // adjusted to fit.
    EXPECT_GE(min_width, w2->GetBoundsInScreen().width());
    EXPECT_NEAR(min_width, w2->GetBoundsInScreen().width(), /*abs_error=*/1);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());

    // Re-snap `w1` to 2/3. Test we keep the group and adjust the bounds.
    SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                      chromeos::kTwoThirdSnapRatio);
    EXPECT_TRUE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    EXPECT_GE(min_width, w2->GetBoundsInScreen().width());
    EXPECT_NEAR(min_width, w2->GetBoundsInScreen().width(), /*abs_error=*/1);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());

    // Re-snap `w2` to 1/3. Test we keep the group and adjust the bounds.
    SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                      chromeos::kOneThirdSnapRatio);
    EXPECT_TRUE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    EXPECT_GE(min_width, w2->GetBoundsInScreen().width());
    EXPECT_NEAR(min_width, w2->GetBoundsInScreen().width(), /*abs_error=*/1);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());
  }
}

// Tests that when both windows have minimum sizes, we don't create a snap
// group.
TEST_F(SnapGroupTest, AutoSnapBothWindowsWithMinimumSizes) {
  // Test with both the bottom and side shelf.
  for (const auto& shelf_alignment :
       {ShelfAlignment::kBottom, ShelfAlignment::kLeft}) {
    std::stringstream ss;
    ss << shelf_alignment;
    SCOPED_TRACE("Shelf alignment = " + ss.str());
    GetPrimaryShelf()->SetAlignment(shelf_alignment);
    // Create `w1` and `w2` both with minimum size 0.6f.
    const gfx::Rect work_area(GetWorkAreaBounds());
    const int min_width = work_area.width() * 0.6f;
    std::unique_ptr<aura::Window> w1(
        CreateAppWindowWithMinSize(gfx::Size(min_width, work_area.height())));
    std::unique_ptr<aura::Window> w2(
        CreateAppWindowWithMinSize(gfx::Size(min_width, work_area.height())));

    // Snap `w1` to 2/3.
    SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                      chromeos::kTwoThirdSnapRatio);
    const gfx::Rect w1_bounds = w1->GetBoundsInScreen();
    EXPECT_TRUE(OverviewController::Get()->InOverviewSession());

    // Select `w2` to be auto-snapped. Since it tries to snap to 0.6f, but `w1`
    // can't fit in the other side, we don't create a group.
    ClickOverviewItem(GetEventGenerator(), w2.get());
    EXPECT_NEAR(min_width, w2->GetBoundsInScreen().width(), 1);
    EXPECT_EQ(w1_bounds.width(), w1->GetBoundsInScreen().width());
    EXPECT_FALSE(
        SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
  }
}

// Test that dragging a window to snap in Overview to activate Partial Overview
// works properly with the existence of Snap Group. Regression test for
// http://b/340931820.
TEST_F(SnapGroupTest, DragToSnapInOverviewWithSnapGroup) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  std::unique_ptr<aura::Window> w3(CreateAppWindow(gfx::Rect()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  // Drag `w3` to snap and verify the partial Overview Grid bounds.
  DragItemToPoint(GetOverviewItemForWindow(w3.get()), gfx::Point(0, 400),
                  GetEventGenerator(), /*by_touch_gestures=*/false,
                  /*drop=*/true);
  EXPECT_TRUE(IsInOverviewSession());
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w3.get())->GetStateType());
  gfx::Rect expected_overview_grid_bounds = GetWorkAreaBounds();
  expected_overview_grid_bounds.Subtract(w3->GetBoundsInScreen());
  EXPECT_EQ(expected_overview_grid_bounds,
            GetOverviewGridBounds(Shell::GetPrimaryRootWindow()));
}

// Tests the behavior in existing partial overview, i.e. overview -> drag to
// snap.
TEST_F(SnapGroupTest, OldPartialOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // Enter overview, then drag to snap. Test we start partial overview.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  auto* event_generator = GetEventGenerator();
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(0, 0),
                       event_generator, /*by_touch_gestures=*/false,
                       /*drop=*/true);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());

  // Select the other window in overview, test we end overview and split view
  // but create a snap group.
  ClickOverviewItem(event_generator, w2.get());
  VerifyNotSplitViewOrOverviewSession(w1.get());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
  MaximizeToClearTheSession(w1.get());

  // Enter overview, then drag `w1` to snap, then drag the other `w2` to snap.
  // Test we start and end overview and split view correctly.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(0, 0),
                       event_generator, /*by_touch_gestures=*/false,
                       /*drop=*/true);
  VerifySplitViewOverviewSession(w1.get());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());
  DragGroupItemToPoint(GetOverviewItemForWindow(w2.get()),
                       GetWorkAreaBounds().top_right(), event_generator,
                       /*by_touch_gestures=*/false,
                       /*drop=*/true);
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  VerifyNotSplitViewOrOverviewSession(w1.get());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
  MaximizeToClearTheSession(w1.get());
  // We need to maximize both windows so that overview isn't started upon
  // conversion to tablet mode.
  // TODO(b/327269057): Investigate tablet mode conversion.
  MaximizeToClearTheSession(w2.get());

  // Test that tablet mode works as normal.
  SwitchToTabletMode();
  EXPECT_FALSE(IsInOverviewSession());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(GetSplitViewController()->InSplitViewMode());
}

// Verifies that with a 3rd window, visually stacked below two snapped
// windows, but placed before the opposite snapped window within the Snap Group
// in MRU order, snapping a 4th window in this setup does not initiate partial
// overview. See http://b/339709601 for details.
TEST_F(SnapGroupTest, RecallSnapGroupWontStartPartialOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  auto* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);

  // Open a 3rd window on top to occlude the snap group.
  std::unique_ptr<aura::Window> w3(CreateAppWindow(GetWorkAreaBounds()));

  // Recall the snap group.
  wm::ActivateWindow(w1.get());
  auto* desk_container = desks_util::GetActiveDeskContainerForRoot(
      Shell::Get()->GetPrimaryRootWindow());

  // Verify the stacking order from bottom to top.
  EXPECT_THAT(desk_container->children(),
              ElementsAre(w3.get(), w2.get(), w1.get(),
                          snap_group->snap_group_divider()
                              ->divider_widget()
                              ->GetNativeWindow()));

  // Open a 4th window and snap it on top. Test we don't start partial overview.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w4.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifyNotSplitViewOrOverviewSession(w4.get());

  // Test the window gets snapped to replace.
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w4.get()));
}

// Verify that 'Search + Shift + G' creates a Snap Group from two snapped
// windows.
TEST_F(SnapGroupTest, UseShortcutToGroupSnappedWindows) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);

  // Press 'Search + Shift + G' to to group `w1` and `w2`.
  auto* event_generator = GetEventGenerator();
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);

  SnapGroupController* snap_group_controller =
      Shell::Get()->snap_group_controller();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Press the shortcut again and the windows will still be grouped.
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
}

// Tests the behavior for an unresizable window that cannot snap.
TEST_F(SnapGroupTest, UnresizableWindowWontFormSnapGroup) {
  std::unique_ptr<aura::Window> normal(CreateAppWindow());
  std::unique_ptr<aura::Window> unresizable(CreateAppWindow());
  unresizable->SetProperty(aura::client::kResizeBehaviorKey,
                           aura::client::kResizeBehaviorNone);

  // 1 - Snap the normal window to start partial overview.
  SnapOneTestWindow(normal.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(normal.get());

  // Selecting the unresizable window from partial overview won't snap and won't
  // add to snap group.
  ClickOverviewItem(GetEventGenerator(), unresizable.get());
  EXPECT_TRUE(ToastManager::Get()->IsToastShown(kAppCannotSnapToastId));
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(normal.get(),
                                                            unresizable.get()));

  // 2 - Snap the unresizable window won't start partial overview and won't
  // form a group even when the layout is complete.
  SnapOneTestWindow(unresizable.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifyNotSplitViewOrOverviewSession(unresizable.get());
  SnapOneTestWindow(normal.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(normal.get(),
                                                            unresizable.get()));
}

// Tests the behavior for an unresizable window that can snap.
TEST_F(SnapGroupTest, UnresizableCanSnapWindowWontFormSnapGroup) {
  std::unique_ptr<aura::Window> normal(CreateAppWindow());
  std::unique_ptr<aura::Window> unresizable(CreateAppWindow());
  unresizable->SetProperty(aura::client::kResizeBehaviorKey,
                           aura::client::kResizeBehaviorNone);
  unresizable->SetProperty(kUnresizableSnappedSizeKey, new gfx::Size(300, 0));

  // 1 - Snap the normal window to start partial overview.
  SnapOneTestWindow(normal.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(normal.get());

  // Select the `kUnresizableSnappedSizeKey` window from partial overview. Test
  // it snaps but does not form a group.
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, unresizable.get());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(unresizable.get())->GetStateType());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(normal.get(),
                                                            unresizable.get()));

  // 2 - Snap the `kUnresizableSnappedSizeKey` window to start partial overview.
  SnapOneTestWindow(unresizable.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  VerifySplitViewOverviewSession(unresizable.get());

  // Select the normal window from partial overview. Test it snaps but does not
  // form a group.
  ClickOverviewItem(event_generator, normal.get());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(normal.get())->GetStateType());
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(normal.get(),
                                                            unresizable.get()));
}

// Tests that re-snapping to the opposite side with a different snap ratio
// updates the bounds correctly. Regression test for http://b/349951979.
TEST_F(SnapGroupTest, ReSnapToOppositeSnapRatio) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Re-snap `w1` to secondary 1/3.
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_EQ(
      std::round(GetWorkAreaBounds().width() * chromeos::kOneThirdSnapRatio),
      w1->GetBoundsInScreen().width());

  // Re-snap `w1` to primary 2/3.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_EQ(
      std::round(GetWorkAreaBounds().width() * chromeos::kTwoThirdSnapRatio),
      w1->GetBoundsInScreen().width());

  // Re-snap `w1` to secondary 1/2.
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_EQ(
      std::round(GetWorkAreaBounds().width() * chromeos::kDefaultSnapRatio),
      w1->GetBoundsInScreen().width());
}

// Tests no dump without crash when one of the windows is minimized. Regression
// test for http://b/352159258.
TEST_F(SnapGroupTest, NoDumpWithoutCrashOnMinimize) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* snap_group_controller = SnapGroupController::Get();
  // Test with both primary and secondary display orientation.
  for (const bool is_layout_primary : {true, false}) {
    if (is_layout_primary) {
      UpdateDisplay("800x600");
    } else {
      UpdateDisplay("800x600/u");
    }
    ASSERT_EQ(is_layout_primary, IsLayoutPrimary(w1.get()));
    SnapOneTestWindow(w1.get(),
                      is_layout_primary ? WindowStateType::kPrimarySnapped
                                        : WindowStateType::kSecondarySnapped,
                      chromeos::kDefaultSnapRatio);
    SnapOneTestWindow(w2.get(),
                      is_layout_primary ? WindowStateType::kSecondarySnapped
                                        : WindowStateType::kPrimarySnapped,
                      chromeos::kDefaultSnapRatio);
    ASSERT_TRUE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    const gfx::Rect w1_bounds(w1->GetBoundsInScreen());
    const gfx::Rect w2_bounds(w2->GetBoundsInScreen());

    // Minimize `w1`.
    auto* window_state1 = WindowState::Get(w1.get());
    window_state1->Minimize();
    ASSERT_FALSE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

    // Verify `w2` is back at 1/2.
    const gfx::Rect work_area(GetWorkAreaBounds());
    gfx::Rect left_half, right_half;
    work_area.SplitVertically(left_half, right_half);
    // Verify `w2` is at the same position, aka approximately the same
    // bounds it was at before.
    EXPECT_TRUE(w2_bounds.ApproximatelyEqual(
        w2->GetBoundsInScreen(),
        /*tolerance=*/kSplitviewDividerShortSideLength / 2));
    EXPECT_EQ(right_half, w2->GetBoundsInScreen());
    auto* window_state2 = WindowState::Get(w2.get());
    EXPECT_EQ(chromeos::kDefaultSnapRatio, window_state2->snap_ratio());

    // Unminimize `w1`. Test the windows are still at 1/2 with no divider.
    window_state1->Unminimize();
    ASSERT_FALSE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    // Verify `w1` is at the same position, aka approximately the same
    // bounds it was at before.
    EXPECT_TRUE(w1_bounds.ApproximatelyEqual(
        w1->GetBoundsInScreen(),
        /*tolerance=*/kSplitviewDividerShortSideLength / 2));
    EXPECT_EQ(left_half, w1->GetBoundsInScreen());
    EXPECT_EQ(chromeos::kDefaultSnapRatio, window_state1->snap_ratio());
    EXPECT_TRUE(w2_bounds.ApproximatelyEqual(
        w2->GetBoundsInScreen(),
        /*tolerance=*/kSplitviewDividerShortSideLength / 2));
    EXPECT_EQ(right_half, w2->GetBoundsInScreen());
    EXPECT_EQ(chromeos::kDefaultSnapRatio, window_state2->snap_ratio());
    MaximizeToClearTheSession(w1.get());
    MaximizeToClearTheSession(w2.get());
  }
}

// Verifies no crashes occur when re-snapping a secondary window (with transient
// child) to the primary position within a Snap Group, triggering a partial
// Overview and subsequent selection of the previous primary window. And the
// transient child window remains visible in partial Overview. Regression test
// for http://b/353574797.
TEST_F(SnapGroupTest, NoCrashWhenReSnappingSecondaryToPrimaryWithTransient) {
  std::unique_ptr<aura::Window> w0(CreateAppWindow(gfx::Rect(0, 0, 300, 300)));
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(500, 0, 300, 300)));
  // Create a bubble widget that's anchored to `w1`.
  auto bubble_delegate1 = std::make_unique<views::BubbleDialogDelegateView>(
      NonClientFrameViewAsh::Get(w1.get()), views::BubbleBorder::TOP_RIGHT);
  // The line below is essential to make sure that the bubble doesn't get closed
  // when entering overview.
  bubble_delegate1->set_close_on_deactivate(false);
  bubble_delegate1->set_parent_window(w1.get());
  views::Widget* bubble_widget1(views::BubbleDialogDelegateView::CreateBubble(
      std::move(bubble_delegate1)));
  bubble_widget1->Show();
  aura::Window* bubble_window1 = bubble_widget1->GetNativeWindow();
  ASSERT_TRUE(bubble_window1->IsVisible());
  ASSERT_TRUE(window_util::AsBubbleDialogDelegate(bubble_window1));

  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  EXPECT_TRUE(bubble_window1->IsVisible());
  EXPECT_FALSE(bubble_window1->GetProperty(chromeos::kIsShowingInOverviewKey));
  EXPECT_FALSE(bubble_window1->GetProperty(kHideInOverviewKey));

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());

  // Induce visibility changes on `bubble_widget1`.
  bubble_widget1->Hide();
  bubble_widget1->Show();
  EXPECT_TRUE(bubble_window1->IsVisible());

  // Verify that there is no crash when selecting OverviewItem for `w0` in
  // partial Overview.
  event_generator->MoveMouseTo(gfx::ToRoundedPoint(
      GetOverviewItemForWindow(w0.get())->target_bounds().CenterPoint()));
  event_generator->ClickLeftButton();
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w0.get()));
}

// -----------------------------------------------------------------------------
// SnapGroupPhantomBoundsTest:

using SnapGroupPhantomBoundsTest = SnapGroupTest;

// Tests that the snap phantom bounds are updated when a window is dragged over
// a snap group.
TEST_F(SnapGroupPhantomBoundsTest, SnapGroupPhantomBounds) {
  UpdateDisplay("800x600");

  // Create a snap group.
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  auto* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  // Resize the snap group to a ratio < kSnapToReplaceRatioDiffThreshold from
  // the default ratio.
  SplitViewDivider* snap_group_divider = snap_group->snap_group_divider();
  const gfx::Point resize_point(
      snap_group_divider->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint());
  snap_group_divider->StartResizeWithDivider(resize_point);
  snap_group_divider->ResizeWithDivider(resize_point - gfx::Vector2d(50, 0));
  snap_group_divider->EndResizeWithDivider(resize_point - gfx::Vector2d(50, 0));
  ASSERT_LE(WindowState::Get(w1.get())->snap_ratio().value() -
                chromeos::kDefaultSnapRatio,
            kSnapToReplaceRatioDiffThreshold);

  // Drag to snap `w3` over `w1`. Test we update the phantom bounds.
  std::unique_ptr<aura::Window> w3 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  DragWindowTo(event_generator, w3.get(), gfx::Point(0, 100),
               /*release=*/false);
  // The phantom bounds will not account for the divider width.
  EXPECT_TRUE(w1->GetBoundsInScreen().ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Drag to snap `w3` over `w2`. Test we update the phantom bounds.
  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(w3.get());
  DragWindowTo(event_generator, w3.get(),
               gfx::Point(work_area.right(), work_area.y() + 100),
               /*release=*/false);
  EXPECT_TRUE(w2->GetBoundsInScreen().ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Create a new window on top of the snap group that fully occludes the snap
  // group. See http://b/347768613 for why phantom bounds for a snap group when
  // there is a partially occluding windows isn't defined.
  std::unique_ptr<aura::Window> w4 = CreateAppWindow(work_area);

  // Now drag to snap `w3`. Since the snap group is not fully visible, the
  // phantom bounds are back to default.
  DragWindowTo(event_generator, w3.get(), gfx::Point(0, 100),
               /*release=*/false);
  EXPECT_EQ(gfx::Rect(0, 0, work_area.width() / 2, work_area.height()),
            WorkspaceWindowResizerTestApi()
                .GetSnapPhantomWindowController()
                ->GetTargetWindowBounds());
}

// Tests that the snap phantom bounds reflect the opposite snapped window.
TEST_F(SnapGroupPhantomBoundsTest, ReflectOppositeSnappedWindow) {
  UpdateDisplay("800x600");

  // Create an app window so it can be recognized by
  // `GetOppositeVisibleSnappedWindow()`, then snap `w1` and resize `w1` to an
  // arbitrary size.
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  auto* event_generator = GetEventGenerator();
  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(w1.get());
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().right_center());
  event_generator->DragMouseTo(250, work_area.CenterPoint().y());
  ASSERT_EQ(250, w1->GetBoundsInScreen().width());

  // Now drag to snap `w2` to the opposite side of `w1`. Test we update the
  // phantom bounds to reflect `w1`.
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  gfx::Rect expected_bounds(work_area);
  expected_bounds.Subtract(w1->GetBoundsInScreen());
  DragWindowTo(event_generator, w2.get(), work_area.right_center(),
               /*release=*/false);
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Test that the window snaps at the phantom bounds.
  event_generator->ReleaseLeftButton();
  ASSERT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w1.get()));
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      w2->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
}

// Tests that the snap phantom bounds work on multi-displays.
TEST_F(SnapGroupPhantomBoundsTest, SnapPhantomBoundsMultiDisplay) {
  UpdateDisplay("800x600,1200x900");

  // Create an app window so it can be recognized by
  // `GetOppositeVisibleSnappedWindow()`, then snap `w1` to 1/3 on display 2.
  std::unique_ptr<aura::Window> w1 =
      CreateAppWindow(gfx::Rect(1200, 0, 400, 400));
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio);
  const display::Display display2 = display_manager()->active_display_list()[1];
  ASSERT_EQ(display2,
            display::Screen::GetScreen()->GetDisplayNearestWindow(w1.get()));

  // Drag to snap `w2` to display 2.
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  const gfx::Rect work_area2 = display2.work_area();
  auto* event_generator = GetEventGenerator();
  DragWindowTo(event_generator, w2.get(), work_area2.left_center(),
               /*release=*/false);

  // Test the phantom bounds are updated on display 2.
  gfx::Rect expected_bounds(work_area2);
  expected_bounds.Subtract(w1->GetBoundsInScreen());
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Test that the window snaps at the phantom bounds.
  event_generator->ReleaseLeftButton();
  ASSERT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w1.get()));
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      w2->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
}

// Tests that the phantom bounds work correctly in portrait mode.
TEST_F(SnapGroupPhantomBoundsTest, SnapPhantomBoundsPortraitMode) {
  UpdateDisplay("600x900");

  // Create an app window so it can be recognized by
  // `GetOppositeVisibleSnappedWindow()`, then snap to 2/3 top.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(w1.get());
  ASSERT_EQ(gfx::Rect(0, 0, work_area.width(),
                      work_area.height() * chromeos::kTwoThirdSnapRatio),
            w1->GetBoundsInScreen());

  // Drag to snap `w2` to the bottom.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  DragWindowTo(event_generator, w2.get(), work_area.bottom_center(),
               /*release=*/false);

  // Test the phantom bounds are updated to reflect `w1`.
  gfx::Rect expected_bounds(work_area);
  expected_bounds.Subtract(w1->GetBoundsInScreen());
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Test that the window snaps at the phantom bounds.
  event_generator->ReleaseLeftButton();
  ASSERT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w1.get()));
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      w2->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
}

// Tests that when the snap phantom bounds window has a minimum size, the
// minimum size is respected.
TEST_F(SnapGroupPhantomBoundsTest, SnapPhantomBoundsMinimumSize) {
  UpdateDisplay("800x600");

  // Create an app window so it can be recognized by
  // `GetOppositeVisibleSnappedWindow()`, then snap to 2/3 left.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(w1.get());
  ASSERT_EQ(gfx::Rect(0, 0, work_area.width() * chromeos::kTwoThirdSnapRatio,
                      work_area.height()),
            w1->GetBoundsInScreen());

  // Set `w2` minimum size to be > 1/3, then drag to snap `window_`.
  const gfx::Size min_size(work_area.width() * 0.4f, work_area.height());
  std::unique_ptr<aura::Window> w2(CreateAppWindowWithMinSize(min_size));
  auto* event_generator = GetEventGenerator();
  DragWindowTo(event_generator, w2.get(), work_area.right_center(),
               /*release=*/false);

  // Test the phantom bounds are at `w2` minimum size.
  gfx::Rect expected_bounds(work_area);
  expected_bounds.set_x(work_area.right() - min_size.width());
  expected_bounds.set_width(min_size.width());
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      WorkspaceWindowResizerTestApi()
          .GetSnapPhantomWindowController()
          ->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Test that the window snaps at the phantom bounds.
  event_generator->ReleaseLeftButton();
  ASSERT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w1.get()));
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      w2->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
}

// Tests that when the snap ratio gap exceeds the threshold, we do not update
// the phantom bounds.
TEST_F(SnapGroupPhantomBoundsTest, SnapRatioGapThreshold) {
  UpdateDisplay("800x600");

  // Snap and resize `w1` so that the snap ratio gap between `w1` and the
  // default snap ratio exceeds the threshold.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  const WindowSnapWMEvent snap_primary(
      WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
      WindowSnapActionSource::kDragWindowToEdgeToSnap);
  WindowState::Get(w1.get())->OnWMEvent(&snap_primary);
  auto* event_generator = GetEventGenerator();
  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(w1.get());
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().right_center());
  event_generator->DragMouseTo(105, work_area.CenterPoint().y());
  const gfx::Rect w1_bounds(w1->GetBoundsInScreen());
  ASSERT_EQ(105, w1_bounds.width());
  ASSERT_GT(std::abs(1.f - *WindowState::Get(w1.get())->snap_ratio() -
                     chromeos::kDefaultSnapRatio),
            kSnapToReplaceRatioDiffThreshold);

  // Drag to snap `w2` on the opposite side. Since we won't auto group, we
  // also don't update the phantom bounds.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  DragWindowTo(event_generator, w2.get(), work_area.right_center(),
               /*release=*/false);
  gfx::Rect expected_bounds(work_area);
  expected_bounds.set_width(work_area.width() / 2);
  expected_bounds.set_x(work_area.width() - expected_bounds.width());
  EXPECT_EQ(expected_bounds, WorkspaceWindowResizerTestApi()
                                 .GetSnapPhantomWindowController()
                                 ->GetTargetWindowBounds());

  // Test the window snaps at the default bounds without forming a group.
  event_generator->ReleaseLeftButton();
  EXPECT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w1.get()));
  EXPECT_EQ(expected_bounds, w2->GetBoundsInScreen());
  EXPECT_EQ(w1_bounds, w1->GetBoundsInScreen());
}

// Tests that the snap phantom bounds are updated correctly after snap to
// replace. Regression test for http://b/349892846.
TEST_F(SnapGroupPhantomBoundsTest, SnapPhantomBoundsAfterSnapToReplace) {
  // Create app windows with big enough bounds, since if the bounds are too
  // small the drag point might conflict with the size button and not start a
  // drag on the caption bar.
  const gfx::Rect work_area(GetWorkAreaBounds());
  std::unique_ptr<aura::Window> w1(CreateAppWindow(work_area));
  std::unique_ptr<aura::Window> w2(CreateAppWindow(work_area));
  std::unique_ptr<aura::Window> w3(CreateAppWindow(work_area));

  // Create a snap group with non-default snap ratio.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  ClickOverviewItem(GetEventGenerator(), w2.get());
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  ASSERT_NEAR(chromeos::kTwoThirdSnapRatio,
              *WindowState::Get(w1.get())->snap_ratio(), 0.01);
  ASSERT_NEAR(chromeos::kOneThirdSnapRatio,
              *WindowState::Get(w2.get())->snap_ratio(), 0.01);

  // Create a 3rd window and drag to snap over `w2` to show the phantom bounds.
  auto* event_generator = GetEventGenerator();
  wm::ActivateWindow(w3.get());
  event_generator->MoveMouseTo(GetDragPoint(w3.get()));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(work_area.right_center());
  ASSERT_TRUE(WindowState::Get(w3.get())->is_dragged());
  auto* snap_phantom_window_controller =
      WorkspaceWindowResizerTestApi().GetSnapPhantomWindowController();
  ASSERT_TRUE(snap_phantom_window_controller);
  EXPECT_TRUE(w2->GetBoundsInScreen().ApproximatelyEqual(
      snap_phantom_window_controller->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Release the drag to snap to replace `w2` in the group.
  event_generator->ReleaseLeftButton();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w3.get()));
  EXPECT_TRUE(w3->GetBoundsInScreen().ApproximatelyEqual(
      w2->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              *WindowState::Get(w3.get())->snap_ratio(), 0.01);

  // Drag to break `w3` from the group.
  event_generator->DragMouseTo(work_area.CenterPoint());
  ASSERT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w3.get()));

  // Activate `w2` to stack it on top of `w3` so it can auto-group with `w1` to
  // simulate the bug.
  wm::ActivateWindow(w2.get());
  ASSERT_TRUE(window_util::IsStackedBelow(w3.get(), w2.get()));

  // Drag to snap `w1` to the opposite side of `w2`.
  wm::ActivateWindow(w1.get());
  event_generator->MoveMouseTo(GetDragPoint(w1.get()));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(work_area.origin());
  ASSERT_TRUE(WindowState::Get(w1.get())->is_dragged());
  snap_phantom_window_controller =
      WorkspaceWindowResizerTestApi().GetSnapPhantomWindowController();
  ASSERT_TRUE(snap_phantom_window_controller);
  gfx::Rect expected_bounds(work_area);
  expected_bounds.Subtract(w2->GetBoundsInScreen());
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      snap_phantom_window_controller->GetTargetWindowBounds(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));

  // Release the drag. Test `w1` snaps at `expected_bounds` and auto-groups with
  // `w2`.
  event_generator->ReleaseLeftButton();
  EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
      w1->GetBoundsInScreen(),
      /*tolerance=*/kSplitviewDividerShortSideLength / 2));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              *WindowState::Get(w1.get())->snap_ratio(), 0.01);
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              *WindowState::Get(w2.get())->snap_ratio(), 0.01);
}

// -----------------------------------------------------------------------------
// SnapGroupFloatTest:

using SnapGroupFloatTest = SnapGroupTest;

// Tests that we can create a Snap Group with a floated window.
TEST_F(SnapGroupFloatTest, SnapGroupCreationWithFloatedWindow) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  std::unique_ptr<aura::Window> normal_window(CreateAppWindow());
  std::unique_ptr<aura::Window> floated_window(CreateAppWindow());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(floated_window.get())->IsFloated());

  SnapOneTestWindow(normal_window.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  WaitForOverviewEntered();
  VerifySplitViewOverviewSession(normal_window.get());
  OverviewItemBase* floated_window_overview_item =
      GetOverviewItemForWindow(floated_window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(gfx::ToRoundedPoint(
      floated_window_overview_item->target_bounds().CenterPoint()));
  event_generator->ClickLeftButton();
  EXPECT_TRUE(floated_window->layer()->GetAnimator()->is_animating());
  EXPECT_NE(floated_window->layer()->transform(),
            floated_window->layer()->GetTargetTransform());
  WaitForOverviewExitAnimation();

  // Verify that Snap Group will be formed after activating `floated_window` in
  // partial Overview.
  EXPECT_FALSE(GetSplitViewController()->InSplitViewMode());
  EXPECT_FALSE(WindowState::Get(floated_window.get())->IsFloated());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
      normal_window.get(), floated_window.get()));
  UnionBoundsEqualToWorkAreaBounds(normal_window.get(), floated_window.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that creating a snap group, then floating a window in the group, then
// re-snapping snaps to the correct bounds. See http://b/349177630 for context.
TEST_F(SnapGroupFloatTest, ReSnapFloatedWindow) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // 1 - Snap `w1` to 2/3 and `w2` to 1/3.
  SnapOneTestWindow(w1.get(), chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  WaitForOverviewEntered();
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, w2.get());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  WaitForOverviewExitAnimation();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Float `w2`.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(w2.get())->IsFloated());
  ASSERT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Drag to snap `w2` to secondary. Test it snaps at 1/3 from the right.
  event_generator->MoveMouseTo(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().right_center());
  EXPECT_TRUE(w2->layer()->GetAnimator()->is_animating());
  EXPECT_NE(w2->layer()->transform(), w2->layer()->GetTargetTransform());
  ASSERT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_EQ(
      std::round(GetWorkAreaBounds().width() * chromeos::kTwoThirdSnapRatio),
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              *WindowState::Get(w1.get())->snap_ratio(), 0.01);
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              *WindowState::Get(w2.get())->snap_ratio(), 0.01);
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Float `w1`.
  wm::ActivateWindow(w1.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(w1.get())->IsFloated());
  ASSERT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Drag to snap `w1` to primary. Test it snaps at 2/3 from the left.
  event_generator->MoveMouseTo(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().left_center());
  ASSERT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(
      std::round(GetWorkAreaBounds().width() * chromeos::kTwoThirdSnapRatio),
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              *WindowState::Get(w1.get())->snap_ratio(), 0.01);
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              *WindowState::Get(w2.get())->snap_ratio(), 0.01);
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// -----------------------------------------------------------------------------
// SnapGroupDividerTest:

using SnapGroupDividerTest = SnapGroupTest;

// Tests that the divider starts with a thin default width
// (`kSplitviewDividerShortSideLength`) in landscape mode, expands to
// `kSplitviewDividerEnlargedShortSideLength` on mouse hover or drag, and
// returns to its default thin width on mouse exit.
TEST_F(SnapGroupDividerTest, HoverToEnlargeDivider) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  auto* divider_view = divider->divider_view_for_testing();
  ASSERT_TRUE(divider_view);
  auto* focus_ring = views::FocusRing::Get(divider_view);
  ASSERT_TRUE(focus_ring);
  auto* handler_view = divider_view->handler_view_for_testing();
  ASSERT_TRUE(handler_view);

  const auto divider_bounds_before_hover =
      divider_widget->GetWindowBoundsInScreen();
  EXPECT_EQ(kSplitviewDividerShortSideLength,
            divider_bounds_before_hover.width());

  const auto handler_view_bounds_before_hover =
      divider_view->GetHandlerViewBoundsInScreenForTesting();
  EXPECT_EQ(kDividerHandlerShortSideLength,
            handler_view_bounds_before_hover.width());
  EXPECT_EQ(kDividerHandlerLongSideLength,
            handler_view_bounds_before_hover.height());

  // Shift the hover point so that it is not right on the divider handler view
  // to trigger hover to enlarge.
  event_generator->MoveMouseTo(
      divider_bounds_before_hover.CenterPoint() +
      gfx::Vector2d(0, kDividerHandlerEnlargedLongSideLength / 2 + 1));
  EXPECT_EQ(kSplitviewDividerEnlargedShortSideLength, divider_view->width());
  const auto handler_view_bounds_on_hover =
      divider_view->GetHandlerViewBoundsInScreenForTesting();
  EXPECT_EQ(kDividerHandlerEnlargedShortSideLength,
            handler_view_bounds_on_hover.width());
  EXPECT_EQ(kDividerHandlerEnlargedLongSideLength,
            handler_view_bounds_on_hover.height());
  EXPECT_FALSE(focus_ring->GetVisible());

  event_generator->MoveMouseBy(10, 0);
  EXPECT_EQ(kSplitviewDividerEnlargedShortSideLength, divider_view->width());
  const auto handler_view_bounds_on_drag =
      divider_view->GetHandlerViewBoundsInScreenForTesting();
  EXPECT_EQ(kDividerHandlerEnlargedShortSideLength,
            handler_view_bounds_on_drag.width());
  EXPECT_EQ(kDividerHandlerEnlargedLongSideLength,
            handler_view_bounds_on_drag.height());
  EXPECT_FALSE(focus_ring->GetVisible());

  event_generator->MoveMouseTo(gfx::Point(0, 0));
  EXPECT_EQ(kSplitviewDividerShortSideLength, divider_view->width());
  const auto handler_view_bounds_after_hover =
      divider_view->GetHandlerViewBoundsInScreenForTesting();
  EXPECT_EQ(kDividerHandlerShortSideLength,
            handler_view_bounds_after_hover.width());
  EXPECT_EQ(kDividerHandlerLongSideLength,
            handler_view_bounds_after_hover.height());
  EXPECT_FALSE(focus_ring->GetVisible());
}

// Tests that the split view divider will be stacked on top of both windows in
// the snap group and that on a third window activated the split view divider
// will be stacked below the newly activated window.
TEST_F(SnapGroupDividerTest, DividerStackingOrderTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  wm::ActivateWindow(w1.get());

  aura::Window* divider_window =
      GetTopmostSnapGroupDivider()->GetDividerWindow();
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), w1.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));

  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(100, 200, 300, 400)));
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w3.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), w1.get()));

  wm::ActivateWindow(w2.get());
  EXPECT_TRUE(window_util::IsStackedBelow(w3.get(), w1.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));
}

// Tests that divider will be closely tied to the windows in a snap group, which
// will also apply on transient window added.
TEST_F(SnapGroupDividerTest, DividerStackingOrderWithTransientWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  wm::ActivateWindow(w1.get());

  aura::Window* divider_window =
      GetTopmostSnapGroupDivider()->GetDividerWindow();
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), w1.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));

  auto w1_transient =
      CreateTransientChildWindow(w1.get(), gfx::Rect(100, 200, 200, 200));
  w1_transient->SetProperty(aura::client::kModalKey,
                            ui::mojom::ModalType::kWindow);
  wm::SetModalParent(w1_transient.get(), w1.get());
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w1_transient.get()));
}

// Tests the overall stacking order with two transient windows each of which
// belongs to a window in snap group is expected. The tests is to verify the
// transient windows issue showed in http://b/297448600#comment2.
TEST_F(SnapGroupDividerTest, DividerStackingOrderWithTwoTransientWindows) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  aura::Window* divider_window =
      GetTopmostSnapGroupDivider()->GetDividerWindow();
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  ASSERT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));

  // By default `w1_transient` is `ModalType::kNone`, meaning that the
  // associated `w1` is interactable.
  std::unique_ptr<aura::Window> w1_transient(
      CreateTransientChildWindow(w1.get(), gfx::Rect(10, 20, 20, 30)));

  // Add transient window for `w2` and making it not interactable by setting it
  // with the type of `ui::mojom::ModalType::kWindow`.
  std::unique_ptr<aura::Window> w2_transient(
      CreateTransientChildWindow(w2.get(), gfx::Rect(200, 20, 20, 30)));
  w2_transient->SetProperty(aura::client::kModalKey,
                            ui::mojom::ModalType::kWindow);
  wm::SetModalParent(w2_transient.get(), w2.get());

  // The expected stacking order is as follows:
  //                    TOP
  // `w2_transient`      |
  //      |              |
  //   divider           |
  //      |              |
  //     `w2`            |
  //      |              |
  // `w1_transient`      |
  //      |              |
  //     `w1`            |
  //                   BOTTOM
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w2_transient.get()));
  EXPECT_TRUE(
      window_util::IsStackedBelow(w1_transient.get(), w2_transient.get()));
  EXPECT_TRUE(window_util::IsStackedBelow(w1_transient.get(), divider_window));
}

// Verifies that if the stacking order of the divider window is altered by
// another dialog transient window, `SplitViewDivider` is able to correct it,
// placing the divider below the dialog transient window. See http://b/341332379
// for more details.
TEST_F(SnapGroupDividerTest,
       DividerStackingOrderWithDialogTransientUndoStacking) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));
  aura::Window* top_window = w2.get();
  aura::Window* top_window_parent = top_window->parent();

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider);
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  aura::Window* divider_window = divider_widget->GetNativeWindow();
  ASSERT_TRUE(wm::HasTransientAncestor(divider_window, w2.get()));
  ASSERT_EQ(top_window_parent, divider_window->parent());

  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  ASSERT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));

  // Create a dialog widget that's associated with `w2`.
  views::DialogDelegateView* delegate = new views::DialogDelegateView();
  views::Widget* widget = views::DialogDelegate::CreateDialogWidget(
      delegate, GetContext(), /*parent=*/w2.get());
  aura::Window* w2_transient = widget->GetNativeWindow();
  ASSERT_TRUE(wm::HasTransientAncestor(w2_transient, w2.get()));
  ASSERT_EQ(top_window_parent, w2_transient->parent());

  // When stacking a child window relative to a target, only the child is
  // notified of stacking order changes (see `Window::StackChildRelativeTo()`).
  // Since `SplitViewDivider` doesn't observe the divider window, we use
  // `StackChildBelow` to ensure the transient window (`w2_transient`) receives
  // this notification, correcting the stacking order so that the divider stays
  // below it.
  top_window_parent->StackChildBelow(w2_transient, divider_window);
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w2_transient));
}

// Tests divider stacking behavior below transient dialog transient of Snap
// Group windows:
//  - During resizing
//  - After clicking post-resize
// Regression test for http://b/349894878.
TEST_F(SnapGroupDividerTest, DividerStackingWhenResizingWithDialogTransient) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider);
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  aura::Window* divider_window = divider_widget->GetNativeWindow();
  ASSERT_TRUE(wm::HasTransientAncestor(divider_window, w2.get()));

  aura::Window* top_window = w2.get();
  aura::Window* top_window_parent = top_window->parent();
  ASSERT_EQ(top_window_parent, divider_window->parent());

  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), w2.get()));
  ASSERT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  ASSERT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));

  // Create a dialog widget that's associated with `w2`.
  views::DialogDelegateView* delegate = new views::DialogDelegateView();
  views::Widget* widget = views::DialogDelegate::CreateDialogWidget(
      delegate, GetContext(), /*parent=*/w2.get());
  aura::Window* w2_transient = widget->GetNativeWindow();
  ASSERT_TRUE(wm::HasTransientAncestor(w2_transient, w2.get()));
  ASSERT_EQ(top_window_parent, w2_transient->parent());

  // Verify that divider remains stacked below the `w2_transient` on resize
  // ended.
  ResizeDividerTo(event_generator,
                  gfx::Point(w2_transient->GetBoundsInScreen().CenterPoint()));
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w2_transient));

  // Click on the divider and it is still stacked below the `w2_transient`.
  event_generator->MoveMouseTo(
      divider_widget->GetWindowBoundsInScreen().top_center());
  event_generator->ClickLeftButton();
  EXPECT_TRUE(window_util::IsStackedBelow(divider_window, w2_transient));
  EXPECT_TRUE(window_util::IsStackedBelow(w1.get(), divider_window));
  EXPECT_TRUE(window_util::IsStackedBelow(w2.get(), divider_window));
}

// Tests that the union bounds of the primary window, secondary window in a snap
// group and the snap group divider will be equal to the work area bounds both
// in horizontal and vertical split view mode.
TEST_F(SnapGroupDividerTest, SnapGroupDividerBoundsTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  for (const auto is_horizontal : {true, false}) {
    if (is_horizontal) {
      UpdateDisplay("900x600");
    } else {
      UpdateDisplay("600x900");
    }

    ASSERT_EQ(IsLayoutHorizontal(w1.get()), is_horizontal);

    SnapTwoTestWindows(w1.get(), w2.get(), is_horizontal, event_generator);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());

    MaximizeToClearTheSession(w1.get());
    MaximizeToClearTheSession(w2.get());
    ASSERT_FALSE(
        SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get()));
  }
}

// Tests that window and divider boundaries adjust correctly with shelf
// auto-hide behavior change.
TEST_F(SnapGroupDividerTest,
       SnapGroupDividerBoundsWithShelfAutoHideBehaviorChange) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);

  Shelf* shelf = GetPrimaryShelf();
  ASSERT_EQ(shelf->auto_hide_behavior(), ShelfAutoHideBehavior::kNever);

  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
  EXPECT_EQ(divider_widget->GetWindowBoundsInScreen().height(),
            GetWorkAreaBounds().height());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), divider);
}

// Tests that snapped windows and divider bounds adjust correctly when shelf
// alignment changes.
TEST_F(SnapGroupDividerTest, SnapGroupDividerBoundsWithShelfAlignmentChange) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);

  Shelf* shelf = GetPrimaryShelf();
  ASSERT_EQ(shelf->alignment(), ShelfAlignment::kBottom);
  for (auto alignment : {ShelfAlignment::kLeft, ShelfAlignment::kRight,
                         ShelfAlignment::kBottom}) {
    shelf->SetAlignment(alignment);
    const gfx::Rect divider_bounds = divider_widget->GetWindowBoundsInScreen();
    EXPECT_EQ(divider_bounds.x(), w1->GetBoundsInScreen().right());
    EXPECT_EQ(divider_bounds.right(), w2->GetBoundsInScreen().x());
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), divider);
  }
}

// Tests that the cursor type gets updated to be resize cursor on mouse hovering
// on the split view divider excluding the feedback button.
TEST_F(SnapGroupDividerTest, CursorUpdateTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider->divider_widget());

  auto divider_bounds = GetTopmostSnapGroupDividerBoundsInScreen();
  auto outside_point = divider_bounds.CenterPoint();
  outside_point.Offset(-kSplitviewDividerShortSideLength * 5, 0);
  EXPECT_FALSE(divider_bounds.Contains(outside_point));

  auto* cursor_manager = Shell::Get()->cursor_manager();
  cursor_manager->SetCursor(CursorType::kPointer);

  // Test that the default cursor type when mouse is not hovered over the split
  // view divider.
  event_generator->MoveMouseTo(outside_point);
  EXPECT_TRUE(cursor_manager->IsCursorVisible());
  EXPECT_FALSE(cursor_manager->IsCursorLocked());
  EXPECT_EQ(CursorType::kNull, cursor_manager->GetCursor().type());

  // Test that the cursor changed to resize cursor while hovering over the split
  // view divider.
  const auto delta_vector = gfx::Vector2d(0, -10);
  const gfx::Point cached_hover_point =
      divider_bounds.CenterPoint() + delta_vector;
  event_generator->MoveMouseTo(cached_hover_point);
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());

  // Test that after resizing, the cursor type is still the resize cursor.
  event_generator->PressLeftButton();
  const auto move_vector = gfx::Vector2d(20, 0);
  event_generator->MoveMouseTo(cached_hover_point + move_vector);
  event_generator->ReleaseLeftButton();
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());
  EXPECT_EQ(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint() + delta_vector,
      cached_hover_point + move_vector);
}

//  Tests that the cursor updates correctly after snap to replace. See
//  regression at http://b/331240308
TEST_F(SnapGroupDividerTest, CursorUpdateAfterSnapToReplace) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Snapping `w3` on top of the snap group and expect the successful
  // snap-to-replace.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  ASSERT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());

  auto divider_bounds = GetTopmostSnapGroupDividerBoundsInScreen();
  auto outside_point = GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  outside_point.Offset(-kSplitviewDividerShortSideLength * 5, 0);
  EXPECT_FALSE(divider_bounds.Contains(outside_point));

  auto* cursor_manager = Shell::Get()->cursor_manager();
  cursor_manager->SetCursor(CursorType::kPointer);

  // Test that the default cursor type when mouse is not hovered over the split
  // view divider.
  event_generator->MoveMouseTo(outside_point);
  EXPECT_TRUE(cursor_manager->IsCursorVisible());
  EXPECT_FALSE(cursor_manager->IsCursorLocked());
  EXPECT_EQ(CursorType::kNull, cursor_manager->GetCursor().type());

  // Test that the cursor changed to resize cursor while hovering over the split
  // view divider.
  const auto delta_vector = gfx::Vector2d(0, -10);
  const gfx::Point cached_hover_point =
      divider_bounds.CenterPoint() + delta_vector;
  event_generator->MoveMouseTo(cached_hover_point);
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());

  // Test that after resizing, the cursor type is still the resize cursor.
  event_generator->PressLeftButton();
  const auto move_vector = gfx::Vector2d(20, 0);
  event_generator->MoveMouseTo(cached_hover_point + move_vector);
  event_generator->ReleaseLeftButton();
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());
  EXPECT_EQ(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint() + delta_vector,
      cached_hover_point + move_vector);
}

// Verify that the cursor changes to `kColumnResize` when hovering over the
// divider handler view in landscape.
TEST_F(SnapGroupDividerTest, CursorUpdateOnHandlerViewInLandscape) {
  UpdateDisplay("900x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider->divider_widget());

  auto divider_bounds = GetTopmostSnapGroupDividerBoundsInScreen();

  auto* cursor_manager = Shell::Get()->cursor_manager();
  const auto center_point = divider_bounds.CenterPoint();

  event_generator->MoveMouseTo(center_point);
  EXPECT_TRUE(cursor_manager->IsCursorVisible());
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());

  event_generator->MoveMouseTo(center_point + gfx::Vector2d(0, 20));
  EXPECT_EQ(CursorType::kColumnResize, cursor_manager->GetCursor().type());
}

// Verify that the cursor changes to `kRowResize` when hovering over the divider
// handler view in portrait.
TEST_F(SnapGroupDividerTest, CursorUpdateOnHandlerViewInPortrait) {
  UpdateDisplay("600x900");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/false, event_generator);
  auto* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider->divider_widget());

  auto divider_bounds = GetTopmostSnapGroupDividerBoundsInScreen();

  auto* cursor_manager = Shell::Get()->cursor_manager();
  const auto center_point = divider_bounds.CenterPoint();

  event_generator->MoveMouseTo(center_point);
  EXPECT_TRUE(cursor_manager->IsCursorVisible());
  EXPECT_EQ(CursorType::kRowResize, cursor_manager->GetCursor().type());

  event_generator->MoveMouseTo(center_point + gfx::Vector2d(20, 0));
  EXPECT_EQ(CursorType::kRowResize, cursor_manager->GetCursor().type());
}

// Tests that the hit area of the snap group divider can be outside of its
// bounds with the extra insets whose value is `kSplitViewDividerExtraInset`.
TEST_F(SnapGroupDividerTest, SnapGroupDividerEnlargedHitArea) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  const gfx::Point cached_divider_center_point =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  gfx::Point hover_location =
      cached_divider_center_point -
      gfx::Vector2d(kSplitviewDividerShortSideLength / 2 +
                        kSplitViewDividerExtraInset / 2,
                    0);
  event_generator->MoveMouseTo(hover_location);
  event_generator->PressLeftButton();
  const auto move_vector = -gfx::Vector2d(50, 0);
  event_generator->MoveMouseTo(hover_location + move_vector);
  event_generator->ReleaseLeftButton();
  // Verify that split view remains inactive to avoid split view specific
  // behaviors such as auto-snap or showing cannot snap toast.
  EXPECT_FALSE(GetSplitViewController()->InSplitViewMode());
  EXPECT_EQ(hover_location + move_vector,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
}

// Tests that a double-tap gesture on the divider handler within a Snap Group
// successfully swaps the two snapped windows.
TEST_F(SnapGroupDividerTest, DoubleTapDividerBasic) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  auto* divider_view = divider->divider_view_for_testing();
  ASSERT_TRUE(divider_view);
  auto* handler_view = divider_view->handler_view_for_testing();
  ASSERT_TRUE(handler_view);

  // A double-tap on the divider handler within a Snap Group swaps the window
  // positions.
  const auto handler_view_center =
      divider_view->GetHandlerViewBoundsInScreenForTesting().CenterPoint();
  event_generator->set_current_screen_location(handler_view_center);
  event_generator->GestureTapAt(handler_view_center);
  event_generator->GestureTapAt(handler_view_center);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  SnapGroup* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  EXPECT_EQ(w1.get(), snap_group->window2());
  EXPECT_EQ(w2.get(), snap_group->window1());
  UnionBoundsEqualToWorkAreaBounds(snap_group);
}

// Tests that double-tap on the Snap Group divider handler swaps the windows
// and their bounds, and that the divider position will adjust correspondingly.
TEST_F(SnapGroupDividerTest, DoubleTapDividerToSwapWindowsBounds) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  auto* divider_view = divider->divider_view_for_testing();
  ASSERT_TRUE(divider_view);
  auto* handler_view = divider_view->handler_view_for_testing();
  ASSERT_TRUE(handler_view);

  auto handler_view_center =
      divider_view->GetHandlerViewBoundsInScreenForTesting().CenterPoint();
  event_generator->set_current_screen_location(handler_view_center);
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(gfx::Point(100, handler_view_center.y()),
                               /*count=*/2);
  event_generator->ReleaseLeftButton();

  const auto w1_snap_ratio_after_drag =
      WindowState::Get(w1.get())->snap_ratio();
  ASSERT_TRUE(w1_snap_ratio_after_drag);
  EXPECT_NE(chromeos::kDefaultSnapRatio, *w1_snap_ratio_after_drag);

  const auto w2_snap_ratio_after_drag =
      WindowState::Get(w2.get())->snap_ratio();
  ASSERT_TRUE(w2_snap_ratio_after_drag);
  EXPECT_NE(chromeos::kDefaultSnapRatio, *w2_snap_ratio_after_drag);

  const gfx::Rect w1_bounds_before_swap = w1->GetBoundsInScreen();
  const gfx::Rect w2_bounds_before_swap = w2->GetBoundsInScreen();
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), divider);

  // Perform double tap on the divider handler and verify that the snapped
  // windows bounds will swap.
  handler_view_center =
      divider_view->GetHandlerViewBoundsInScreenForTesting().CenterPoint();
  event_generator->GestureTapAt(handler_view_center);
  event_generator->GestureTapAt(handler_view_center);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  SnapGroup* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  EXPECT_EQ(w1.get(), snap_group->window2());
  EXPECT_EQ(w2.get(), snap_group->window1());

  EXPECT_EQ(w1_bounds_before_swap.width(),
            snap_group->window2()->GetBoundsInScreen().width());
  EXPECT_EQ(w2_bounds_before_swap.width(),
            snap_group->window1()->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(snap_group);
}

// Tests that a double-tap gesture on the divider handler within a Snap Group
// successfully swaps the two snapped windows, each of which has a transient
// window attached.
TEST_F(SnapGroupDividerTest, DoubleTapDividerWithTransient) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);

  // By default transient is `ModalType::kNone`, meaning that the associated
  // window is interactable.
  std::unique_ptr<aura::Window> w1_transient(
      CreateTransientChildWindow(w1.get(), gfx::Rect(10, 20, 20, 30)));
  std::unique_ptr<aura::Window> w2_transient(
      CreateTransientChildWindow(w2.get(), gfx::Rect(510, 30, 50, 30)));

  // A double-tap on the divider handler within a Snap Group swaps the window
  // positions.
  const auto divider_center_point =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  event_generator->set_current_screen_location(divider_center_point);
  event_generator->GestureTapAt(divider_center_point);
  event_generator->GestureTapAt(divider_center_point);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  SnapGroup* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  EXPECT_EQ(w1.get(), snap_group->window2());
  EXPECT_TRUE(
      wm::HasTransientAncestor(w1_transient.get(), snap_group->window2()));
  EXPECT_EQ(w2.get(), snap_group->window1());
  EXPECT_TRUE(
      wm::HasTransientAncestor(w2_transient.get(), snap_group->window1()));
  UnionBoundsEqualToWorkAreaBounds(snap_group);
}

// Tests that performing a double-tap gesture during a divider drag operation
// does not cause crash.
TEST_F(SnapGroupDividerTest, DoubleTapWhileDraggingDivider) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);

  const auto divider_center_point_0 =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  event_generator->PressTouchId(
      /*touch_id=*/0, divider_center_point_0);

  // Use the initial tap to drag the divider.
  event_generator->MoveTouchId(gfx::Point(100, divider_center_point_0.y()), 0);
  ASSERT_TRUE(divider->is_resizing_with_divider());

  // Trigger double tap, using a different `touch_id`.
  const auto divider_center_point_1 =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  event_generator->PressTouchId(
      /*touch_id=*/1, divider_center_point_1);
  event_generator->PressTouchId(
      /*touch_id=*/1, divider_center_point_1);

  base::RunLoop().RunUntilIdle();
}

TEST_F(SnapGroupDividerTest, DoubleTapDividerInTablet) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group = SnapGroupController::Get()->GetTopmostSnapGroup();
  EXPECT_TRUE(snap_group);
  auto* new_primary_window = snap_group->window1();
  auto* new_secondary_window = snap_group->window2();

  // Switch to tablet mode. Test that double tap on the divider swaps the
  // windows.
  SwitchToTabletMode();
  EXPECT_EQ(new_primary_window, GetSplitViewController()->primary_window());
  EXPECT_EQ(new_secondary_window, GetSplitViewController()->secondary_window());
  EXPECT_TRUE(GetSplitViewDivider()->divider_widget());
  const gfx::Point divider_center =
      GetSplitViewDivider()
          ->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint();
  event_generator->GestureTapAt(divider_center);
  event_generator->GestureTapAt(divider_center);
  EXPECT_EQ(new_secondary_window, GetSplitViewController()->primary_window());
  EXPECT_EQ(new_primary_window, GetSplitViewController()->secondary_window());
}

// Tests that when the cursor is moved significantly past the window sizes
// then moved the other direction, we don't update bounds.
TEST_F(SnapGroupDividerTest, ResizeCursor) {
  const int min_width = 300;
  std::unique_ptr<aura::Window> w1(
      CreateAppWindowWithMinSize(gfx::Size(min_width, min_width)));
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_divider = SnapGroupController::Get()
                                 ->GetSnapGroupForGivenWindow(w1.get())
                                 ->snap_group_divider();
  for (const auto& display_specs : {"800x600", "600x800"}) {
    UpdateDisplay(display_specs);
    const auto display = display::Screen::GetScreen()->GetPrimaryDisplay();
    // Press and move the mouse left without releasing, past `w1`'s min width.
    // Test we don't update bounds beyond `w1`'s min width.
    const gfx::Point divider_point(
        snap_group_divider->GetDividerBoundsInScreen(/*is_dragging=*/false)
            .CenterPoint());
    event_generator->set_current_screen_location(divider_point);
    event_generator->PressLeftButton();
    const bool horizontal = IsLayoutHorizontal(display);
    const gfx::Point resize_point1 = horizontal
                                         ? gfx::Point(10, divider_point.y())
                                         : gfx::Point(divider_point.x(), 10);
    event_generator->MoveMouseTo(resize_point1, /*count=*/2);
    ASSERT_TRUE(snap_group_divider->is_resizing_with_divider());
    EXPECT_EQ(min_width, GetWindowLength(w1.get(), horizontal));
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

    // Move the mouse right but still beyond `w1`'s min width. Test we don't
    // update bounds yet.
    const gfx::Point resize_point2 = horizontal
                                         ? gfx::Point(150, divider_point.y())
                                         : gfx::Point(divider_point.x(), 150);
    event_generator->MoveMouseTo(resize_point2, /*count=*/2);
    EXPECT_EQ(min_width, GetWindowLength(w1.get(), horizontal));
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

    // Move the mouse over `w1`'s min width. Test we update bounds now.
    const gfx::Point resize_point3 =
        horizontal ? gfx::Point(min_width, divider_point.y())
                   : gfx::Point(divider_point.x(), min_width);
    event_generator->MoveMouseTo(resize_point3,
                                 /*count=*/2);
    EXPECT_EQ(min_width, GetWindowLength(w1.get(), horizontal));
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

    // Move the mouse past `w1`'s min width. Test we update bounds now.
    const gfx::Point resize_point4 = horizontal
                                         ? gfx::Point(600, divider_point.y())
                                         : gfx::Point(divider_point.x(), 600);
    event_generator->MoveMouseTo(resize_point4,
                                 /*count=*/2);
    EXPECT_EQ(
        600,
        horizontal
            ? GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x()
            : GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().y());
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
    event_generator->ReleaseLeftButton();
  }
}

// -----------------------------------------------------------------------------
// SnapGroupOverviewTest:
using SnapGroupOverviewTest = SnapGroupTest;

TEST_F(SnapGroupOverviewTest, OverviewEnterExitBasic) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Verify that full overview session is expected when starting overview from
  // accelerator and that split view divider will not be available.
  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  WaitForOverviewEnterAnimation();
  EXPECT_TRUE(overview_controller->overview_session());
  EXPECT_EQ(GetOverviewGridBounds(w1->GetRootWindow()), GetWorkAreaBounds());
  EXPECT_FALSE(GetTopmostSnapGroupDivider()->divider_widget()->IsVisible());
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());

  // Verify that the snap group is restored with two windows snapped and that
  // the snap group divider becomes available on overview exit.
  ToggleOverview();
  EXPECT_FALSE(overview_controller->overview_session());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that partial overview is shown on the other side of the screen on one
// window snapped.
TEST_F(SnapGroupOverviewTest, PartialOverview) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  auto* root_window = w1->GetRootWindow();
  for (const auto& snap_state :
       {WindowStateType::kPrimarySnapped, WindowStateType::kSecondarySnapped}) {
    SnapOneTestWindow(w1.get(), snap_state, chromeos::kDefaultSnapRatio);
    WaitForOverviewEnterAnimation();
    EXPECT_TRUE(OverviewController::Get()->overview_session());
    EXPECT_NE(GetOverviewGridBounds(root_window), GetWorkAreaBounds());
    EXPECT_NEAR(GetOverviewGridBounds(root_window).width(),
                GetWorkAreaBounds().width() / 2.f,
                kSplitviewDividerShortSideLength / 2.f);
  }
}

// Tests that the group item will be created properly and that the snap group
// will be represented as one group item in overview.
TEST_F(SnapGroupOverviewTest, OverviewGroupItemCreationBasic) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(),
                     /*horizontal=*/true, GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  WaitForOverviewEnterAnimation();
  ASSERT_TRUE(overview_controller->overview_session());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
}

// Verifies that the divider doesn't appear precipitously before the exit
// animation of the two windows in overview mode is complete, guaranteeing a
// seamless transition. See regression at http://b/333465871.
TEST_F(SnapGroupOverviewTest, DividerExitOverviewAnimation) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider);
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  ASSERT_TRUE(divider_widget->IsVisible());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  WaitForOverviewEntered();
  EXPECT_TRUE(divider_widget);
  EXPECT_FALSE(divider_widget->IsVisible());

  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, GetEventGenerator());
  SendKey(ui::VKEY_RETURN, GetEventGenerator(), 0);

  // Verify that `divider_widget` remains invisible until overview exit
  // animation is complete.
  EXPECT_TRUE(divider_widget);
  EXPECT_FALSE(divider_widget->IsVisible());
  WaitForOverviewExitAnimation();
  EXPECT_TRUE(divider_widget);
  EXPECT_TRUE(divider_widget->IsVisible());
}

// Tests that if one of the windows in a snap group gets destroyed in overview,
// the overview group item will only host the other window. If both of the
// windows get destroyed, the corresponding overview group item will be removed
// from the overview grid.
TEST_F(SnapGroupOverviewTest, WindowDestructionInOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  WaitForOverviewEnterAnimation();
  ASSERT_TRUE(overview_controller->overview_session());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  ASSERT_EQ(2u, overview_grid->item_list().size());

  // On one window in snap group destroying, the group item will host the other
  // window.
  w2.reset();
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());

  // On the only remaining window in snap group destroying, the group item will
  // be removed from the overview grid.
  w1.reset();
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(1u, overview_grid->item_list().size());
}

// Tests that the rounded corners of the remaining item in the snap group on
// window destruction will be refreshed so that the exposed corners will be
// rounded corners.
TEST_F(SnapGroupOverviewTest, RefreshVisualsOnWindowDestructionInOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  ASSERT_TRUE(overview_controller->overview_session());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& overview_items = overview_grid->item_list();
  ASSERT_EQ(2u, overview_items.size());

  w2.reset();
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());

  for (const auto& overview_item : overview_items) {
    const gfx::RoundedCornersF rounded_corners =
        overview_item->GetRoundedCorners();
    EXPECT_NEAR(rounded_corners.upper_left(), kWindowMiniViewCornerRadius,
                /*abs_error=*/0.01);
    EXPECT_NEAR(rounded_corners.upper_right(), kWindowMiniViewCornerRadius,
                /*abs_error=*/0.01);
    EXPECT_NEAR(rounded_corners.lower_right(), kWindowMiniViewCornerRadius,
                /*abs_error=*/0.01);
    EXPECT_NEAR(rounded_corners.lower_left(), kWindowMiniViewCornerRadius,
                /*abs_error=*/0.01);
  }
}

// Tests that when one of the window in snap group gets destroyed in overview,
// the other window will restore its bounds properly when been activated to exit
// overview.
TEST_F(SnapGroupOverviewTest,
       RemainingWindowBoundsRestoreAfterDestructionInOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  // Note here `w1` would have been shrunk for the divider width.
  const gfx::Size w1_size_before_overview = w1->GetBoundsInScreen().size();

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  ASSERT_TRUE(overview_controller->InOverviewSession());
  EXPECT_FALSE(w1->transform().IsIdentity());
  EXPECT_FALSE(w2->transform().IsIdentity());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  ASSERT_EQ(2u, overview_grid->item_list().size());

  // On one window in snap group destroying, the group item will host the other
  // window.
  w2.reset();
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());

  ClickOverviewItem(GetEventGenerator(), w1.get());
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_FALSE(
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get()));
  const gfx::Size w1_size_after_overview = w1->GetBoundsInScreen().size();

  // Verify that w1 is restored to its pre-overview bounds and any
  // divider-related margin adjustments have been reverted.
  EXPECT_EQ(
      w1_size_before_overview.width() + kSplitviewDividerShortSideLength / 2.f,
      w1_size_after_overview.width());

  // Verify that the transform is identity.
  EXPECT_TRUE(w1->transform().IsIdentity());
}

// Tests that the individual items within the same group will be hosted by the
// same overview group item.
TEST_F(SnapGroupOverviewTest, OverviewItemTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(overview_session);

  EXPECT_EQ(overview_session->GetOverviewItemForWindow(w1.get()),
            overview_session->GetOverviewItemForWindow(w2.get()));
}

// Tests that the size of the `OverviewItem`s hosted by the `OverviewGroupItem`
// will correspond to the actual window layout.
TEST_F(SnapGroupOverviewTest, ReflectSnapRatioInOverviewGroupItem) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  const gfx::Point hover_location =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  GetTopmostSnapGroupDivider()->StartResizeWithDivider(hover_location);
  const gfx::Vector2d drag_delta(-GetWorkAreaBounds().width() / 6, 0);
  const auto end_point = hover_location + drag_delta;
  GetTopmostSnapGroupDivider()->ResizeWithDivider(end_point);
  GetTopmostSnapGroupDivider()->EndResizeWithDivider(end_point);
  // Verify that split view remains inactive to avoid split view specific
  // behaviors such as auto-snap or showing cannot snap toast.
  EXPECT_FALSE(GetSplitViewController()->InSplitViewMode());
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              WindowState::Get(w1.get())->snap_ratio().value(),
              /*abs_error=*/0.01);
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              WindowState::Get(w2.get())->snap_ratio().value(),
              /*abs_error=*/0.01);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(overview_session);

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w1.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  // Since `w1` is roughly half the width of `w2`, verify that `item1_bounds` is
  // also half the width of `item2_bounds`.
  const gfx::RectF item1_bounds = overview_items[0]->target_bounds();
  const gfx::RectF item2_bounds = overview_items[1]->target_bounds();
  const float size_ratio =
      static_cast<float>(item1_bounds.width()) / item2_bounds.width();
  EXPECT_NEAR(size_ratio, 0.5, /*abs_error=*/0.05);
}

// Tests that snap group restores to its original snap ratio after on Overview
// exit.
TEST_F(SnapGroupOverviewTest, RestoreSnapRatioOnOverviewExit) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());

  // Drag the divider between the snapped windows to get the 1/3 and 2/3 split
  // screen.
  const gfx::Point hover_location =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  GetTopmostSnapGroupDivider()->StartResizeWithDivider(hover_location);
  const gfx::Vector2d drag_delta(-GetWorkAreaBounds().width() / 6, 0);
  const auto end_point = hover_location + drag_delta;
  GetTopmostSnapGroupDivider()->ResizeWithDivider(end_point);
  GetTopmostSnapGroupDivider()->EndResizeWithDivider(end_point);

  WindowState* w1_window_state = WindowState::Get(w1.get());
  WindowState* w2_window_state = WindowState::Get(w2.get());

  auto w1_snap_ratio_before = w1_window_state->snap_ratio();
  ASSERT_TRUE(w1_snap_ratio_before.has_value());
  auto w2_snap_ratio_before = w2_window_state->snap_ratio();
  ASSERT_TRUE(w2_snap_ratio_before.has_value());
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio, *w1_snap_ratio_before,
              /*abs_error=*/0.01);
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio, *w2_snap_ratio_before,
              /*abs_error=*/0.01);

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  ToggleOverview();
  ASSERT_FALSE(IsInOverviewSession());

  // Both of the windows restored to their original snap ratio on Overview exit.
  auto w1_snap_ratio_after = w1_window_state->snap_ratio();
  ASSERT_TRUE(w1_snap_ratio_after.has_value());
  auto w2_snap_ratio_after = w2_window_state->snap_ratio();
  ASSERT_TRUE(w2_snap_ratio_after.has_value());
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio, *w1_snap_ratio_after,
              /*abs_error=*/0.01);
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio, *w2_snap_ratio_after,
              /*abs_error=*/0.01);
}

// Tests the individual close functionality of the `OverviewGroupItem` by
// clicking on the close button of each overview item.
TEST_F(SnapGroupOverviewTest, CloseIndividualWindowByCloseButton) {
  ScopedOverviewTransformWindow::SetImmediateCloseForTests(/*immediate=*/true);
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());
  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w0.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  // Since the window will be deleted in overview, release the ownership to
  // avoid double deletion.
  w0.release();

  const CloseButton* w0_close_button =
      overview_items[0]->overview_item_view()->close_button();
  event_generator->MoveMouseTo(
      w0_close_button->GetBoundsInScreen().CenterPoint());
  event_generator->ClickLeftButton();

  // Use the run loop so that to wait until the window is closed.
  base::RunLoop().RunUntilIdle();

  // Verify that only one item remains to be hosted by the group item.
  ASSERT_EQ(overview_items.size(), 1u);

  // Verify that the visuals of the remaining item will be refreshed with four
  // rounded corners applied.
  const gfx::RoundedCornersF rounded_corners =
      GetOverviewItemForWindow(w1.get())->GetRoundedCorners();
  EXPECT_NEAR(rounded_corners.upper_left(), kWindowMiniViewCornerRadius,
              /*abs_error=*/1);
  EXPECT_NEAR(rounded_corners.upper_right(), kWindowMiniViewCornerRadius,
              /*abs_error=*/1);
  EXPECT_NEAR(rounded_corners.lower_right(), kWindowMiniViewCornerRadius,
              /*abs_error=*/1);
  EXPECT_NEAR(rounded_corners.lower_left(), kWindowMiniViewCornerRadius,
              /*abs_error=*/1);
}

// Test some basic keyboard traversal on a snap group in overview.
TEST_F(SnapGroupOverviewTest, TabbingBasic) {
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());
  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w0.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  OverviewFocusCycler* focus_cycler =
      overview_controller->overview_session()->focus_cycler();
  // Tab through and verify each individual snap group item.
  PressAndReleaseKey(ui::VKEY_TAB);
  EXPECT_EQ(overview_items[0]->overview_item_view(),
            focus_cycler->GetOverviewFocusedView());

  PressAndReleaseKey(ui::VKEY_TAB);
  EXPECT_EQ(overview_items[1]->overview_item_view(),
            focus_cycler->GetOverviewFocusedView());

  // Press return which will activate the snap group and exit overview.
  PressAndReleaseKey(ui::VKEY_RETURN);
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_TRUE(wm::IsActiveWindow(w1.get()));
}

// Within an overview group, verify that "Ctrl + W" closes only the focused
// item's window, and that tabbing afterwards activates remaining items without
// causing a crash. See http://b/344216297 for more details.
TEST_F(SnapGroupOverviewTest, CtrlPlusWToCloseFocusedItemInGroupInOverview) {
  // Explicitly enable immediate close so that we can directly close the
  // window(s) without waiting the delayed task to be completed in
  // `ScopedOverviewTransformWindow::Close()`.
  ScopedOverviewTransformWindow::SetImmediateCloseForTests(/*immediate=*/true);

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(GetOverviewItemForWindow(w0.get()));

  auto* event_generator = GetEventGenerator();
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  EXPECT_TRUE(overview_session->focus_cycler()->GetOverviewFocusedView());

  // Since the window will be deleted in overview, release the ownership to
  // avoid double deletion.
  w0.release();

  // Press `Ctrl + w` to close `w0`.
  SendKey(ui::VKEY_W, event_generator, ui::EF_CONTROL_DOWN);

  // Verify that `w0` in the snap group will be deleted.
  EXPECT_FALSE(w0.get());
  EXPECT_TRUE(w1.get());

  // Native widget close is not immediate. Run `base::RunLoop().RunUntilIdle()`
  // to ensure close task to finish before proceeding.
  base::RunLoop().RunUntilIdle();

  // Tab again to focus on the remaining window `w1`.
  SendKey(ui::VKEY_TAB, event_generator);

  // Press enter key to active `w1`, verify that there will be no crash.
  SendKey(ui::VKEY_RETURN, event_generator);
  EXPECT_FALSE(overview_controller->InOverviewSession());
}

// Tests that the bounds on the overview group item as well as the individual
// overview item hosted by the group item will be set correctly.
TEST_F(SnapGroupOverviewTest, OverviewItemBoundsTest) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_TRUE(wm::IsActiveWindow(w2.get()));

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests);
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(overview_session);

  // The cumulative sum of the bounds while iterating through the individual
  // items hosted by the overview item should always be inside the group item
  // widget target bounds.
  auto* overview_group_item =
      overview_session->GetOverviewItemForWindow(w1.get());
  const gfx::RectF& group_item_bounds = overview_group_item->target_bounds();
  gfx::RectF cumulative_bounds;
  for (aura::Window* window : overview_group_item->GetWindows()) {
    auto* overview_item = overview_session->GetOverviewItemForWindow(window);
    cumulative_bounds.Union(overview_item->target_bounds());
    EXPECT_GT(cumulative_bounds.width(), 0u);
    EXPECT_TRUE(group_item_bounds.Contains(cumulative_bounds));
  }
}

// Tests the rounded corners will be applied to the exposed corners of the
// overview group item in horizontal wndow layout.
TEST_F(SnapGroupOverviewTest, OverviewGroupItemRoundedCornersInHorizontal) {
  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  std::unique_ptr<aura::Window> window2 = CreateAppWindow(gfx::Rect(100, 100));
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(2u, item_list.size());
  for (const auto& overview_item : item_list) {
    EXPECT_EQ(overview_item->GetRoundedCorners(),
              gfx::RoundedCornersF(kWindowMiniViewCornerRadius));
  }
}

// Tests that if two windows are both snapped before entering overview but not
// in a Snap Group, re-snapping the windows on both sides of the screen will not
// result in crash and two windows will be grouped. See http://b/346617565 for
// more details
TEST_F(SnapGroupOverviewTest, ReSnapSnappedWindowInOverview) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  std::unique_ptr<aura::Window> w2 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  WMEvent minimize_event(WM_EVENT_MINIMIZE);
  WindowState::Get(w2.get())->OnWMEvent(&minimize_event);

  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  OverviewItemBase* overview_item1 = GetOverviewItemForWindow(w1.get());
  OverviewItemBase* overview_item2 = GetOverviewItemForWindow(w2.get());

  DragItemToPoint(overview_item1, gfx::Point(0, 200), event_generator);
  DragItemToPoint(overview_item2, gfx::Point(800, 200), event_generator);
  VerifyNotSplitViewOrOverviewSession(w1.get());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests the rounded corners will be applied to the exposed corners of the
// overview group item in vertical wndow layout.
TEST_F(SnapGroupOverviewTest, OverviewGroupItemRoundedCornersInVertical) {
  UpdateDisplay("600x900");
  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  std::unique_ptr<aura::Window> window2 = CreateAppWindow(gfx::Rect(100, 100));
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/false,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(2u, item_list.size());
  for (const auto& overview_item : item_list) {
    EXPECT_EQ(overview_item->GetRoundedCorners(),
              gfx::RoundedCornersF(kWindowMiniViewCornerRadius));
  }
}

// Tests that the shadow for the group item in overview will be applied on the
// group-level.
TEST_F(SnapGroupOverviewTest, OverviewGroupItemShadow) {
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow(gfx::Rect(100, 100)));
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->overview_session());
  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(2u, item_list.size());

  // Wait until the post task to `UpdateRoundedCornersAndShadow()` triggered in
  // `OverviewController::DelayedUpdateRoundedCornersAndShadow()` is finished.
  ShellTestApi().WaitForOverviewAnimationState(
      OverviewAnimationState::kEnterAnimationComplete);
  base::RunLoop().RunUntilIdle();
  for (const auto& overview_item : item_list) {
    const auto shadow_content_bounds =
        overview_item->get_shadow_content_bounds_for_testing();
    ASSERT_FALSE(shadow_content_bounds.IsEmpty());
    EXPECT_EQ(shadow_content_bounds.size(),
              gfx::ToRoundedSize(overview_item->target_bounds().size()));
  }
}

// Tests that when one of the windows in the snap group gets destroyed in
// overview the shadow contents bounds on the remaining item get updated
// correctly.
TEST_F(SnapGroupOverviewTest, CorrectShadowBoundsOnRemainingItemInOverview) {
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Create more windows to ensure the position of the `OverviewGroupItem` needs
  // to be updated during the Overview grid re-layout since the Overview grid
  // layout is left-aligned.
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(100, 100, 200, 100)));
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 200, 100, 200)));
  std::unique_ptr<aura::Window> w4(
      CreateAppWindow(gfx::Rect(100, 200, 200, 300)));
  std::unique_ptr<aura::Window> w5(
      CreateAppWindow(gfx::Rect(200, 100, 300, 200)));

  OverviewController* overview_controller = Shell::Get()->overview_controller();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->overview_session());
  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(5u, item_list.size());

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(item_list[4].get());
  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  w0.reset();
  EXPECT_EQ(item_list.size(), 5u);
  EXPECT_EQ(overview_items.size(), 1u);

  // Verify that the group-level shadow will be reset and the window-level
  // shadow bounds of the remaining item is refreshed to fit with the remaining
  // item.
  auto* group_shadow = overview_group_item->shadow_for_testing();
  EXPECT_FALSE(group_shadow);

  auto* window1_shadow = overview_items[0]->shadow_for_testing();
  ASSERT_TRUE(window1_shadow);
  EXPECT_EQ(gfx::ToRoundedSize(overview_group_item->target_bounds().size()),
            window1_shadow->GetContentBounds().size());
}

// Tests the basic functionality of activating a group item in overview with
// mouse or touch. Overview will exit upon mouse/touch release and the overview
// item that directly handles the event will be activated.
TEST_F(SnapGroupOverviewTest, GroupItemActivation) {
  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);
  // Pre-check that `window1` is the active window between the windows in the
  // snap group.
  ASSERT_TRUE(wm::IsActiveWindow(window1.get()));
  std::unique_ptr<aura::Window> window2 = CreateAppWindow(gfx::Rect(100, 100));
  ASSERT_TRUE(wm::IsActiveWindow(window2.get()));

  struct {
    bool use_touch;
    gfx::Vector2d offset;
    raw_ptr<aura::Window> expected_activated_window;
  } kTestCases[]{
      {false, gfx::Vector2d(-10, 0), window0.get()},
      {true, gfx::Vector2d(-10, 0), window0.get()},
      {false, gfx::Vector2d(10, 0), window1.get()},
      {true, gfx::Vector2d(10, 0), window1.get()},
  };

  OverviewController* overview_controller = OverviewController::Get();

  for (const auto& test : kTestCases) {
    overview_controller->StartOverview(OverviewStartAction::kTests,
                                       OverviewEnterExitType::kImmediateEnter);
    ASSERT_TRUE(overview_controller->InOverviewSession());

    const auto* overview_grid =
        GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
    ASSERT_TRUE(overview_grid);
    const auto& item_list = overview_grid->item_list();
    ASSERT_EQ(2u, item_list.size());

    OverviewSession* overview_session = overview_controller->overview_session();
    auto* overview_item =
        overview_session->GetOverviewItemForWindow(window0.get());
    const auto hover_point =
        gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()) +
        test.offset;
    event_generator->set_current_screen_location(hover_point);
    if (test.use_touch) {
      event_generator->PressTouch();
      event_generator->ReleaseTouch();
    } else {
      event_generator->ClickLeftButton();
    }

    EXPECT_FALSE(overview_controller->InOverviewSession());

    // Verify that upon mouse/touch release, the snap group will be brought to
    // the front with the expected activated.
    EXPECT_TRUE(wm::IsActiveWindow(test.expected_activated_window));
  }
}

// Tests the basic drag and drop functionality for overview group item with both
// mouse and touch events. The group item will be dropped to its original
// position before drag started.
TEST_F(SnapGroupOverviewTest, DragAndDropBasic) {
  // Explicitly create another desk so that the virtual desk bar won't expand
  // from zero-state to expanded-state when dragging starts.
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());

  OverviewSession* overview_session = overview_controller->overview_session();
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window0.get());
  const auto target_bounds_before_dragging = overview_item->target_bounds();

  for (const bool by_touch : {false, true}) {
    DragGroupItemToPoint(
        overview_item,
        Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
        event_generator, by_touch, /*drop=*/false);
    EXPECT_NE(overview_item->target_bounds(), target_bounds_before_dragging);

    if (by_touch) {
      event_generator->ReleaseTouch();
    } else {
      event_generator->ReleaseLeftButton();
    }

    EXPECT_TRUE(overview_controller->InOverviewSession());

    // Verify that `overview_item` is dropped to its old position before
    // dragging.
    EXPECT_EQ(overview_item->target_bounds(), target_bounds_before_dragging);
  }
}

// Tests that the bounds of the drop target for `OverviewGroupItem` will match
// that of the corresponding item which the drop target is a placeholder for.
TEST_F(SnapGroupOverviewTest, DropTargetBoundsForGroupItem) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  aura::Window* primary_root_window = Shell::GetPrimaryRootWindow();
  auto* overview_grid = GetOverviewGridForRoot(primary_root_window);
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());

  OverviewSession* overview_session = overview_controller->overview_session();
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window0.get());
  const gfx::RectF target_bounds_before_dragging =
      overview_item->target_bounds();

  for (const bool by_touch : {false, true}) {
    DragGroupItemToPoint(
        overview_item,
        Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
        event_generator, by_touch, /*drop=*/false);
    EXPECT_TRUE(overview_controller->InOverviewSession());

    auto* drop_target = overview_grid->drop_target();
    ASSERT_TRUE(drop_target);

    // Verify that the bounds of the `drop_target` will be the same as the
    // `target_bounds_before_dragging`.
    EXPECT_EQ(gfx::RectF(drop_target->target_bounds()),
              target_bounds_before_dragging);
    if (by_touch) {
      event_generator->ReleaseTouch();
    } else {
      event_generator->ReleaseLeftButton();
    }
  }
}

// Tests the stacking order of the overview group item should be above other
// overview items while being dragged.
TEST_F(SnapGroupOverviewTest, StackingOrderWhileDraggingInOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> w0 = CreateAppWindow();
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  std::unique_ptr<aura::Window> w2 = CreateAppWindow(gfx::Rect(100, 100));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(2u, item_list.size());

  OverviewSession* overview_session = overview_controller->overview_session();
  auto* group_item = overview_session->GetOverviewItemForWindow(w0.get());
  auto* group_item_widget = group_item->item_widget();
  auto* w2_item_pre_drag = GetOverviewItemForWindow(w2.get());
  EXPECT_TRUE(window_util::IsStackedBelow(
      w2_item_pre_drag->item_widget()->GetNativeWindow(),
      group_item_widget->GetNativeWindow()));

  // Initiate the first drag.
  DragGroupItemToPoint(
      group_item,
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/false);
  EXPECT_TRUE(overview_controller->InOverviewSession());

  auto* w2_item_during_drag = GetOverviewItemForWindow(w2.get());
  auto* w2_item_window_during_drag =
      w2_item_during_drag->item_widget()->GetNativeWindow();

  // Verify that the two windows together with the group item widget will be
  // stacked above the other overview item.
  EXPECT_TRUE(window_util::IsStackedBelow(
      w2_item_window_during_drag, group_item_widget->GetNativeWindow()));
  EXPECT_TRUE(
      window_util::IsStackedBelow(w2_item_window_during_drag, w0.get()));
  EXPECT_TRUE(
      window_util::IsStackedBelow(w2_item_window_during_drag, w1.get()));
  event_generator->ReleaseLeftButton();

  // Verify that the group item can be dragged again after completing the first
  // drag.
  DragGroupItemToPoint(
      group_item,
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/true);
  EXPECT_TRUE(overview_controller->InOverviewSession());
}

// Verify that when dragging the group item, the close buttons of the individual
// items within the group are disabled with opacity set to 0, and their opacity
// is restored once the drag ends.
TEST_F(SnapGroupOverviewTest, HideCloseButtonsOnDragStart) {
  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  auto* window_widget0 = views::Widget::GetWidgetForNativeView(window0.get());
  views::test::TestWidgetObserver observer0(window_widget0);
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(window0.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  // On drag starts, the close buttons of the individual items are disabled and
  // their opacity is set to 0.
  DragGroupItemToPoint(
      overview_group_item,
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/false);
  for (const auto& item : overview_items) {
    auto* close_button = item->overview_item_view()->close_button();
    ASSERT_TRUE(item->overview_item_view()->close_button());
    EXPECT_EQ(close_button->layer()->GetTargetOpacity(), 0.f);
    EXPECT_FALSE(close_button->GetEnabled());
  }

  // On the drag and drop completes, the close buttons of the individual items
  // restore to their default state.
  event_generator->ReleaseLeftButton();
  for (const auto& item : overview_items) {
    auto* close_button = item->overview_item_view()->close_button();
    ASSERT_TRUE(item->overview_item_view()->close_button());
    EXPECT_EQ(close_button->layer()->GetTargetOpacity(), 1.f);
    EXPECT_TRUE(close_button->GetEnabled());
  }
}

// Tests that if one of `OverviewItem`s hosted by the `OverviewGroupItem` has a
// focus ring before being dragged, the focus ring is cleared when dragging
// begins.
TEST_F(SnapGroupOverviewTest, ClearFocusOnDragStart) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> w0 = CreateAppWindow();
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());

  auto* group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w0.get()));
  const auto& overview_items = group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  OverviewFocusCycler* focus_cycler =
      overview_controller->overview_session()->focus_cycler();
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  EXPECT_EQ(overview_items[0]->overview_item_view(),
            focus_cycler->GetOverviewFocusedView());

  // Verify that the focus ring gets cleared on drag starts.
  DragGroupItemToPoint(
      group_item,
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/false);

  EXPECT_TRUE(overview_controller->InOverviewSession());
  EXPECT_FALSE(focus_cycler->GetOverviewFocusedView());
  event_generator->ReleaseLeftButton();
}

// Tests that converting to tablet mode while dragging an `OverviewGroupItem`
// doesn't result in crash. Regression test for http://b/359942514.
TEST_F(SnapGroupOverviewTest, ConvertToTabletModeWhileDragging) {
  std::unique_ptr<aura::Window> w0 = CreateAppWindow();
  std::unique_ptr<aura::Window> w1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());

  auto* group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w0.get()));
  const auto& overview_items = group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  DragGroupItemToPoint(
      group_item,
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/false);

  EXPECT_TRUE(overview_controller->InOverviewSession());

  SwitchToTabletMode();
  base::RunLoop().RunUntilIdle();
}

// Tests that fling-to-close gestures on `OverviewGroupItem` closes the windows
// in the Snap Group.
TEST_F(SnapGroupOverviewTest, FlingToCloseGroupItem) {
  // Explicitly enable immediate close so that we can directly close the
  // window(s) without waiting the delayed task to be completed in
  // `ScopedOverviewTransformWindow::Close()`.
  ScopedOverviewTransformWindow::SetImmediateCloseForTests(true);

  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  auto* window_widget0 = views::Widget::GetWidgetForNativeView(window0.get());
  views::test::TestWidgetObserver observer0(window_widget0);
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* window_widget1 = views::Widget::GetWidgetForNativeView(window1.get());
  views::test::TestWidgetObserver observer1(window_widget1);
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton,
                                     OverviewEnterExitType::kImmediateEnter);
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(overview_session);

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(window0.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());

  // Pre-release ownership of `window0` and `window1` using `release()`. This is
  // crucial to avoid double-freeing memory. When unique_ptr goes out of scope,
  // its destructor will attempt to deallocate the owned memory. Since CloseAll
  // will already handle the window destruction, leaving the unique_ptrs to
  // manage the memory would lead to a second deallocation attempt on the same
  // address, resulting in crash.
  window0.release();
  window1.release();

  gfx::PointF location = overview_group_item->target_bounds().CenterPoint();
  location.Offset(/*delta_x=*/-10, /*delta_y=*/0);

  overview_session->InitiateDrag(overview_group_item, location,
                                 /*is_touch_dragging=*/true,
                                 /*event_source_item=*/overview_items[0].get());

  // Perform fling-to-close with vertical velocity greater than
  // `kFlingToCloseVelocityThreshold` to trigger fling-to-close.
  overview_session->Fling(overview_group_item, location, /*velocity_x=*/0,
                          /*velocity_y=*/2500);

  // Widget closure is asynchronous and may not finish immediately. For
  // guaranteed completion, run the current thread's RunLoop until idle (See
  // `NativeWidgetAura::Close()` for details).
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(observer0.widget_closed());
  EXPECT_TRUE(observer1.widget_closed());

  // Verify that Overview exits with no Overview items exist.
  EXPECT_FALSE(IsInOverviewSession());
}

// Tests that `OverviewGroupItem` is not snappable in overview when there are
// two windows hosted by it however when one of the windows gets destroyed in
// overview, the remaining item becomes snappable.
TEST_F(SnapGroupOverviewTest, GroupItemSnapBehaviorInOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kTests,
                                     OverviewEnterExitType::kImmediateEnter);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  auto* overview_grid = GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());

  OverviewSession* overview_session = overview_controller->overview_session();
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window0.get());
  const auto target_bounds_before_dragging = overview_item->target_bounds();
  const auto drag_point =
      Shell::GetPrimaryRootWindow()->GetBoundsInScreen().left_center();
  DragGroupItemToPoint(overview_item, drag_point, event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);

  DragGroupItemToPoint(overview_item, drag_point, event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);
  EXPECT_FALSE(overview_item->get_cannot_snap_widget_for_testing());
  EXPECT_TRUE(overview_controller->InOverviewSession());

  // Verify that `overview_item` is dropped to its old position before
  // dragging.
  EXPECT_EQ(overview_item->target_bounds(), target_bounds_before_dragging);

  // Reset `window0` and verify that the remaining item becomes snappable.
  window0.reset();

  DragGroupItemToPoint(
      overview_session->GetOverviewItemForWindow(window1.get()), drag_point,
      event_generator, /*by_touch_gestures=*/false, /*drop=*/true);
  EXPECT_EQ(WindowState::Get(window1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
}

// Tests that in non-primary display orientations, the visuals of the Snap Group
// item in Overview accurately represent the actual layout of the windows in the
// group, and that stepping through the windows in the Overview also follows the
// correct layout order. See http://b/339023083 for more details about the
// issue.
TEST_F(SnapGroupOverviewTest, OverviewGroupItemForNonPrimaryScreenOrientation) {
  // Update display to be in non-primary portrait mode.
  UpdateDisplay("1200x900/r");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(1U, displays.size());
  ASSERT_EQ(chromeos::OrientationType::kPortraitSecondary,
            chromeos::GetDisplayCurrentOrientation(displays[0]));

  std::unique_ptr<aura::Window> window2 = CreateAppWindow(gfx::Rect(200, 200));
  std::unique_ptr<aura::Window> window1 = CreateAppWindow(gfx::Rect(100, 100));
  std::unique_ptr<aura::Window> window0 = CreateAppWindow(gfx::Rect(10, 10));

  // Drag `window0` to the **top** of the screen to snap it into the
  // **secondary** position, as the display is currently oriented in secondary
  // portrait mode.
  SnapOneTestWindow(window0.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_TRUE(IsInOverviewSession());

  auto* event_generator = GetEventGenerator();
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  event_generator->PressKey(ui::VKEY_RETURN, /*flags=*/0);
  ASSERT_TRUE(SnapGroupController::Get()->AreWindowsInSnapGroup(window0.get(),
                                                                window1.get()));

  // With non-primary layout, this is the layout of `window0` and `window1`.
  //              +-----------+
  //              |           |
  //              |    w0     |
  //              |           |
  //              |-----------|
  //              |           |
  //              |    w1     |
  //              |           |
  //              +-----------+

  // Verify the windows bounds i.e. `window0` is on top (secondary snapped) and
  // `window1` in on bottom (primary snapped).
  gfx::Rect work_area = GetWorkAreaBoundsForWindow(window0.get());
  const gfx::Rect divider_bounds =
      GetTopmostSnapGroupDivider()->divider_widget()->GetWindowBoundsInScreen();
  EXPECT_EQ(gfx::Rect(work_area.x(), work_area.y(), work_area.width(),
                      divider_bounds.y()),
            window0->GetBoundsInScreen());
  EXPECT_EQ(gfx::Rect(work_area.x(), divider_bounds.bottom(), work_area.width(),
                      work_area.height() - divider_bounds.bottom()),
            window1->GetBoundsInScreen());

  EXPECT_TRUE(wm::IsActiveWindow(window1.get()));

  std::unique_ptr<aura::Window> window3 =
      CreateAppWindow(gfx::Rect(300, 300), chromeos::AppType::CHROME_APP);
  EXPECT_TRUE(wm::IsActiveWindow(window3.get()));

  ToggleOverview();

  // Overview item list:
  // window3, [window0, window1], window2
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  event_generator->PressKey(ui::VKEY_TAB, /*flags=*/0);
  event_generator->PressKey(ui::VKEY_RETURN, /*flags=*/0);
  EXPECT_TRUE(wm::IsActiveWindow(window0.get()));
}

TEST_F(SnapGroupOverviewTest, SkipPairingInOverviewWhenClickingEmptyArea) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  WaitForOverviewEnterAnimation();
  OverviewController* overview_controller = OverviewController::Get();
  EXPECT_TRUE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  ASSERT_EQ(1u, GetOverviewSession()->grid_list().size());

  auto* w2_overview_item = GetOverviewItemForWindow(w2.get());
  EXPECT_TRUE(w2_overview_item);
  const gfx::Point outside_point =
      gfx::ToRoundedPoint(
          w2_overview_item->GetTransformedBounds().bottom_right()) +
      gfx::Vector2d(20, 20);

  // Verify that clicking on an empty area in overview will exit the paring.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(outside_point);
  event_generator->ClickLeftButton();
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  EXPECT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

TEST_F(SnapGroupOverviewTest, SkipPairingInOverviewWithEscapeKey) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  OverviewController* overview_controller = OverviewController::Get();
  EXPECT_TRUE(overview_controller->InOverviewSession());
  EXPECT_TRUE(GetOverviewSession()->IsWindowInOverview(w2.get()));
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  ASSERT_EQ(1u, GetOverviewSession()->grid_list().size());

  GetEventGenerator()->PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  EXPECT_FALSE(overview_controller->InOverviewSession());
  EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
            WindowStateType::kPrimarySnapped);
  EXPECT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// This test validates `OverviewItemFillMode` restrictions on
// `OverviewGroupItem`:
//   - Windows within an `OverviewGroupItem` are prohibited from using special
//   `OverviewItemFillMode`s (`kPillarBoxed`, `kLetterBoxed`).
//   - This restriction is in place to avoid visual glitches and header
//   misalignment problems that can occur when one window in the group is
//   resized to a very narrow or wide aspect ratio.
// See http://b/341750824 for more details.
TEST_F(SnapGroupOverviewTest, OverviewItemFillMode) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  const gfx::Point divider_center(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  event_generator->MoveMouseTo(divider_center);
  event_generator->PressLeftButton();

  // Resize the divider to the extreme left. This makes the left snapped window
  // so narrow that it will be the `OverviewItemFillMode::kPillarBoxed` if it is
  // an independent window.
  event_generator->MoveMouseTo(gfx::Point(10, 200), /*count=*/2);
  event_generator->ReleaseLeftButton();

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w1.get()));
  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  for (const auto& overview_item : overview_items) {
    // As we disallow setting special `OverviewItemFillMode` for windows in a
    // Snap Group, the left snapped window will maintain the default
    // `OverviewItemFillMode::kNormal` fill mode.
    EXPECT_EQ(OverviewItemFillMode::kNormal,
              overview_item->GetOverviewItemFillMode());
  }
}

// Verifies bubble transient windows hide in Overview, reappear on Overview
// exit, while other transient windows (unless `kHideInOverviewKey` is set to
// true) remain visible.
TEST_F(SnapGroupOverviewTest, HideBubbleTransientInOverview) {
  std::unique_ptr<aura::Window> w0(CreateAppWindow(gfx::Rect(0, 0, 300, 300)));
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(500, 20, 200, 200)));
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Create a bubble widget that's anchored to `w0`.
  auto bubble_delegate0 = std::make_unique<views::BubbleDialogDelegateView>(
      NonClientFrameViewAsh::Get(w0.get()), views::BubbleBorder::TOP_RIGHT);

  // The line below is essential to make sure that the bubble doesn't get closed
  // when entering overview.
  bubble_delegate0->set_close_on_deactivate(false);
  bubble_delegate0->set_parent_window(w0.get());
  views::Widget* bubble_widget0(views::BubbleDialogDelegateView::CreateBubble(
      std::move(bubble_delegate0)));
  aura::Window* bubble_window0 = bubble_widget0->GetNativeWindow();
  ASSERT_TRUE(window_util::AsBubbleDialogDelegate(bubble_window0));

  bubble_widget0->Show();
  EXPECT_TRUE(wm::HasTransientAncestor(bubble_window0, w0.get()));

  // Verify that the bubble is created inside its anchor widget.
  EXPECT_TRUE(
      w0->GetBoundsInScreen().Contains(bubble_window0->GetBoundsInScreen()));

  // By default `w1_transient` is `ModalType::kNone`.
  std::unique_ptr<aura::Window> w1_transient(
      CreateTransientChildWindow(w1.get(), gfx::Rect(510, 30, 50, 30)));
  wm::AddTransientChild(w1.get(), w1_transient.get());

  // Verify that bubble transient windows are hidden on entering Overview mode.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_FALSE(bubble_window0->IsVisible());
  EXPECT_TRUE(w1_transient->IsVisible());

  // Verify that bubble transient windows reappear on exiting Overview mode.
  ToggleOverview();
  ASSERT_FALSE(IsInOverviewSession());

  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w0.get(), w1.get()));
  EXPECT_TRUE(bubble_window0->IsVisible());
  EXPECT_TRUE(w1_transient->IsVisible());
}

// Test that duplicate group items are not created when one of the windows in
// Snap Group has an activatable transient window. Regression test for
// http://b/349482229.
TEST_F(SnapGroupOverviewTest, NoDuplicateGroupItemsWithActivatableTransient) {
  UpdateDisplay("900x600");

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // By default `w1_transient` is `ModalType::kNone`, meaning that the
  // associated `w1` is interactable.
  auto w1_transient =
      CreateTransientChildWindow(w1.get(), gfx::Rect(600, 200, 200, 200));
  w1_transient->SetProperty(aura::client::kModalKey,
                            ui::mojom::ModalType::kWindow);
  wm::SetModalParent(w1_transient.get(), w1.get());

  wm::ActivateWindow(w0.get());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  auto* overview_grid = GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);

  // Verify that there will be only one Overview item in the list.
  EXPECT_EQ(1u, overview_grid->item_list().size());
}

// -----------------------------------------------------------------------------
// SnapGroupDesksTest:
using SnapGroupDesksTest = SnapGroupTest;

// Tests that the two windows contained in the overview group item will be moved
// from the original desk to another desk on drag complete and that the two
// windows will still be in a snap group. The divider will show up in the
// destination desk on target desk activated.
TEST_F(SnapGroupDesksTest, DragOverviewGroupItemToAnotherDesk) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window0 = CreateAppWindow();
  std::unique_ptr<aura::Window> window1 = CreateAppWindow();
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     event_generator);

  ASSERT_TRUE(EnterOverview(OverviewEnterExitType::kImmediateEnter));

  auto* overview_grid = GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());
  const auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);
  const auto& mini_views = desks_bar_view->mini_views();
  ASSERT_EQ(2u, mini_views.size());

  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);

  // Verify the initial conditions before dragging the item to another desk.
  ASSERT_EQ(desks_util::GetDeskForContext(window0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(window1.get()), desk0);

  // Test that both windows contained in the overview group item will be moved
  // to the another desk.
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  DragGroupItemToPoint(
      overview_controller->overview_session()->GetOverviewItemForWindow(
          window0.get()),
      mini_views[1]->GetBoundsInScreen().CenterPoint(), event_generator,
      /*by_touch_gestures=*/false,
      /*drop=*/true);
  EXPECT_TRUE(overview_controller->InOverviewSession());
  ASSERT_EQ(desks_util::GetDeskForContext(window0.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(window1.get()), desk1);
  EXPECT_TRUE(SnapGroupController::Get()->AreWindowsInSnapGroup(window0.get(),
                                                                window1.get()));
  ActivateDesk(desk1);
  EXPECT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  EXPECT_EQ(desks_util::GetDeskForContext(
                GetTopmostSnapGroupDivider()->GetDividerWindow()),
            desk1);
}

// Verify that there will be no crash when dragging the group item with the
// existence of bubble widget to another desk in overview. See the crash at
// http://b/311255082.
TEST_F(SnapGroupDesksTest,
       NoCrashWhenDraggingOverviewGroupItemWithBubbleToAnotherDesk) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> w0(CreateAppWindow(gfx::Rect(0, 0, 300, 300)));
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(500, 20, 200, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  // Create a dummy view for the bubble, adding it to the `w0`.
  views::Widget* w0_widget = views::Widget::GetWidgetForNativeWindow(w0.get());
  auto* child_view =
      w0_widget->GetRootView()->AddChildView(std::make_unique<views::View>());
  child_view->SetBounds(100, 10, 20, 20);

  // Create a bubble widget that's anchored to `w0`.
  auto bubble_delegate = std::make_unique<views::BubbleDialogDelegateView>(
      child_view, views::BubbleBorder::TOP_RIGHT);

  // The line below is essential to make sure that the bubble doesn't get closed
  // when entering overview.
  bubble_delegate->set_close_on_deactivate(false);
  views::Widget* bubble_widget(views::BubbleDialogDelegateView::CreateBubble(
      std::move(bubble_delegate)));
  aura::Window* bubble_window = bubble_widget->GetNativeWindow();
  wm::AddTransientChild(w0.get(), bubble_window);

  bubble_widget->Show();
  EXPECT_TRUE(wm::HasTransientAncestor(bubble_window, w0.get()));

  // Verify that the bubble is created inside its anchor widget.
  EXPECT_TRUE(
      w0->GetBoundsInScreen().Contains(bubble_window->GetBoundsInScreen()));

  ASSERT_TRUE(EnterOverview(OverviewEnterExitType::kImmediateEnter));

  auto* overview_grid = GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());
  const auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);
  const auto& mini_views = desks_bar_view->mini_views();
  ASSERT_EQ(2u, mini_views.size());

  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);

  // Verify the initial conditions before dragging the item to another desk.
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  // Test that both windows contained in the overview group item are contained
  // in `desk1` after the drag.
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  DragGroupItemToPoint(
      overview_controller->overview_session()->GetOverviewItemForWindow(
          w0.get()),
      mini_views[1]->GetBoundsInScreen().CenterPoint(), event_generator,
      /*by_touch_gestures=*/false,
      /*drop=*/true);
  EXPECT_TRUE(overview_controller->InOverviewSession());
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk1);
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w0.get(), w1.get()));
}

// Test: Dragging an `OverviewGroupItem` between desk containers (both
// containing `OverviewGroupItem`)
//  - Verify that an `OverviewGroupItem` can be dragged from one desk container
//  to another when both containers already have `OverviewGroupItem` present.
//  - Ensure no crashes occur during the process.
//  - Confirm that the OverviewGroupItem is reparented to the new desk
//  container.
// See http://b/333613078 for more details about the crash.
TEST_F(SnapGroupDesksTest, DragOverviewGroupItemToAnotherDeskWithSnapGroup) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);

  std::unique_ptr<aura::Window> w0(CreateAppWindow(gfx::Rect(0, 0, 300, 300)));
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(500, 20, 200, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  ActivateDesk(desk1);
  std::unique_ptr<aura::Window> w2(CreateAppWindow(gfx::Rect(0, 0, 100, 100)));
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 20, 100, 200)));
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  ASSERT_EQ(desks_util::GetDeskForContext(w2.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(w3.get()), desk1);

  ASSERT_TRUE(EnterOverview());

  auto* overview_grid = GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);
  const auto& mini_views = desks_bar_view->mini_views();
  ASSERT_EQ(mini_views.size(), 2u);

  // Test that both windows contained in the overview group item will be moved
  // to the `desk0` and no crash on activating `desk0`.
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  DragGroupItemToPoint(
      overview_controller->overview_session()->GetOverviewItemForWindow(
          w3.get()),
      mini_views[0]->GetBoundsInScreen().CenterPoint(), event_generator,
      /*by_touch_gestures=*/false,
      /*drop=*/true);
  EXPECT_TRUE(overview_controller->InOverviewSession());
  EXPECT_EQ(desks_util::GetDeskForContext(w2.get()), desk0);
  EXPECT_EQ(desks_util::GetDeskForContext(w3.get()), desk0);
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w0.get(), w1.get()));
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w2.get(), w3.get()));
  ActivateDesk(desk0);
}

// Tests that if one of the windows parent container change in snap group, the
// other window will follow and get moved to the same target desk container. To
// simulate the CUJ, `Search + Shift + [ or ]` is used in the test to trigger
// moving active window to the left / right desk
TEST_F(SnapGroupDesksTest, WindowDeskContainerChange) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  // Use `Search + Shift + ]` to move `w0` and `w1` to `desk1`.
  PressAndReleaseKey(ui::VKEY_OEM_6, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(desks_util::GetDeskForContext(w0.get()), desk1);
  EXPECT_EQ(desks_util::GetDeskForContext(w1.get()), desk1);

  ActivateDesk(desk1);

  // Use `Search + Shift + [` to move `w0` and `w1` to `desk0`
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  EXPECT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);
}

// Tests that switching desks in Overview mode with a Snap Group using a
// keyboard shortcut does not end the Overview session. Also verify that
// returning to the original desk where the Snap Group belongs does not re-snap
// the windows. See regression at http://b/334221711.
TEST_F(SnapGroupDesksTest, DeskSwitchingInOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  ASSERT_TRUE(IsInOverviewSession());

  // Use `Search + ]` to switch to `desk1`.
  PressAndReleaseKey(ui::VKEY_OEM_6, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(desk1->is_active());

  // Use `Search + [` to switch to `desk0`.
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_TRUE(desk0->is_active());

  auto* overview_group_item = GetOverviewItemForWindow(w0.get());
  ASSERT_TRUE(overview_group_item);

  // Activate the group item and verify the union bounds.
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  PressAndReleaseKey(ui::VKEY_RETURN);
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(),
                                   GetTopmostSnapGroupDivider());
}

// Ensures no crashes occur when switching and merging desks with an active Snap
// Group in Overview. Regression test for http://b/348067578.
TEST_F(SnapGroupDesksTest, DesksSwitchingThenMergingInOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  // Create `snap_group0` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  ToggleOverview();
  WaitForOverviewEntered();
  ASSERT_TRUE(IsInOverviewSession());

  // Use `Search + ]` to switch to `desk1`.
  PressAndReleaseKey(ui::VKEY_OEM_6, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(IsInOverviewSession());

  // Merge `desk0` into `desk1`.
  RemoveDesk(desk0, DeskCloseType::kCombineDesks);

  ASSERT_TRUE(IsInOverviewSession());
  auto* overview_group_item = GetOverviewItemForWindow(w0.get());
  ASSERT_TRUE(overview_group_item);

  // Verify that windows in `snap_group0` have been moved to `desk1`.
  EXPECT_EQ(1u, desks_controller->desks().size());
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w1.get()));

  // Activate the group item and verify the union bounds.
  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, event_generator);
  PressAndReleaseKey(ui::VKEY_RETURN);
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(),
                                   GetTopmostSnapGroupDivider());
}

// Ensures switching and merging desks with one Snap Group on each desk works
// properly in Overview.
TEST_F(SnapGroupDesksTest,
       DesksSwitchingThenMergingWithOneSnapGroupPerDeskInOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  // Create `snap_group0` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group0 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group0);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  // Create `snap_group1` on `desk1`.
  ActivateDesk(desk1);
  std::unique_ptr<aura::Window> w2(CreateAppWindow(gfx::Rect(0, 0, 100, 100)));
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 20, 100, 200)));
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group1 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w2.get());
  ASSERT_TRUE(snap_group1);
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w2.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w3.get()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  // Use `Search + [` to switch to `desk0`.
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(IsInOverviewSession());

  // Merge `desk0` into `desk1`.
  RemoveDesk(desk0, DeskCloseType::kCombineDesks);

  ASSERT_TRUE(IsInOverviewSession());
  auto* overview_group_item = GetOverviewItemForWindow(w0.get());
  ASSERT_TRUE(overview_group_item);

  // Verify that windows in the Snap Group have been moved to `desk1`.
  EXPECT_EQ(1u, desks_controller->desks().size());
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w1.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w2.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w3.get()));
}

TEST_F(SnapGroupDesksTest, DeskSwitchingWithKeyboardShortcut) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  // Use `Search + ]` to switch to `desk1`, `w0` and `w1` will remain on `desk0`
  // with `TargetVisibility()` equals to true.
  PressAndReleaseKey(ui::VKEY_OEM_6, ui::EF_COMMAND_DOWN);
  EXPECT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  EXPECT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);
  EXPECT_TRUE(w0->TargetVisibility());
  EXPECT_TRUE(w1->TargetVisibility());

  // Use `Search + [` to switch back to `desk0`, `w0` and `w1` will remain on
  // `desk0` with `TargetVisibility()` equals to true.
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_COMMAND_DOWN);
  EXPECT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  EXPECT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);
  EXPECT_TRUE(w0->TargetVisibility());
  EXPECT_TRUE(w1->TargetVisibility());
}

// Tests that after a Snap Group is resized and then moved to a different desk,
// the relative positions of the snapped windows within the group remain
// unchanged. See the regression details at http://b/335303673.
TEST_F(SnapGroupDesksTest, ResizeThenMoveGroupToAnotherDesk) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  auto* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider);
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);

  event_generator->MoveMouseTo(
      divider_widget->GetWindowBoundsInScreen().CenterPoint());

  event_generator->DragMouseBy(100, 0);
  const gfx::Rect cached_w0_bounds(w0->GetBoundsInScreen());
  const gfx::Rect cached_w1_bounds(w1->GetBoundsInScreen());

  ASSERT_TRUE(EnterOverview(OverviewEnterExitType::kImmediateEnter));

  auto* overview_grid = GetOverviewGridForRoot(w0->GetRootWindow());
  ASSERT_TRUE(overview_grid);
  const auto& item_list = overview_grid->item_list();
  ASSERT_EQ(1u, item_list.size());
  const auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);
  const auto& mini_views = desks_bar_view->mini_views();
  ASSERT_EQ(2u, mini_views.size());

  OverviewController* overview_controller = Shell::Get()->overview_controller();
  DragGroupItemToPoint(
      overview_controller->overview_session()->GetOverviewItemForWindow(
          w0.get()),
      mini_views[1]->GetBoundsInScreen().CenterPoint(), event_generator,
      /*by_touch_gestures=*/false,
      /*drop=*/true);

  EXPECT_TRUE(overview_controller->InOverviewSession());
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk1);

  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w0.get(), w1.get()));
  ActivateDesk(desk1);
  ASSERT_TRUE(divider_widget);
  EXPECT_EQ(desks_util::GetDeskForContext(divider_widget->GetNativeWindow()),
            desk1);

  EXPECT_EQ(cached_w0_bounds, w0->GetBoundsInScreen());
  EXPECT_EQ(cached_w1_bounds, w1->GetBoundsInScreen());
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(), divider);
}

// Tests that pressing the 'Close All' button closes both windows in a Snap
// Group.
TEST_F(SnapGroupDesksTest, CloseAll) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  ASSERT_TRUE(desk0->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  auto* window_widget0 = views::Widget::GetWidgetForNativeView(w0.get());
  views::test::TestWidgetObserver observer0(window_widget0);
  auto* window_widget1 = views::Widget::GetWidgetForNativeView(w1.get());
  views::test::TestWidgetObserver observer1(window_widget1);

  // Pre-release ownership of `w0` and `w1` using `release()`. This is crucial
  // to avoid double-freeing memory. When unique_ptr goes out of scope, its
  // destructor will attempt to deallocate the owned memory. Since CloseAll will
  // already handle the window destruction, leaving the unique_ptrs to manage
  // the memory would lead to a second deallocation attempt on the same address,
  // resulting in crash.
  w0.release();
  w1.release();

  RemoveDesk(desk0, DeskCloseType::kCloseAllWindows);
  EXPECT_EQ(1u, desks_controller->desks().size());

  // Widget closure is asynchronous and may not finish immediately. For
  // guaranteed completion, run the current thread's RunLoop until idle (See
  // `NativeWidgetAura::Close()` for details).
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(observer0.widget_closed());
  EXPECT_TRUE(observer1.widget_closed());
}

// Verifies Snap Group behavior during desk removal and undo: windows become
// invisible during removal, and clicking 'Undo' restores them to their original
// desk.
TEST_F(SnapGroupDesksTest, DeskRemovalAndUndo) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  RemoveDesk(desk0, DeskCloseType::kCloseAllWindowsAndWait);
  ASSERT_TRUE(desk0->is_desk_being_removed());
  // `w0` and `w1` will remain invisible while the desk is being removed.
  EXPECT_FALSE(w0->IsVisible());
  EXPECT_FALSE(w1->IsVisible());

  // Restoring desk0 will also restore the visibility of `w0` and `w1`.
  views::LabelButton* dismiss_button =
      DesksTestApi::GetCloseAllUndoToastDismissButton();
  ASSERT_TRUE(dismiss_button);
  LeftClickOn(dismiss_button);
  EXPECT_TRUE(w0->IsVisible());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  EXPECT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));
}

// Tests that when an active desk is removed by keyboard shortcut `Search +
// Shift + -`, the Snap Group will be moved to the next available desk. The
// divider widget is visible when not in overview mode, and hidden when in
// overview mode. See the bug at http://b/335300918.
TEST_F(SnapGroupDesksTest, DeskRemovalAndEnterOverview) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  // Use `Search + Shift + -` to remove `desk0`.
  DeskSwitchAnimationWaiter waiter;
  PressAndReleaseKey(ui::VKEY_OEM_MINUS,
                     ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
  waiter.Wait();

  // Verify that both `w0` and `w1` will be moved to `desk1`.
  EXPECT_TRUE(desk1->is_active());
  EXPECT_EQ(1u, desks_controller->desks().size());
  EXPECT_EQ(desk1, desks_util::GetDeskForContext(w0.get()));
  EXPECT_EQ(desk1, desks_util::GetDeskForContext(w1.get()));
  EXPECT_TRUE(snap_group);
  auto* divider = snap_group->snap_group_divider();
  auto* divider_widget = divider->divider_widget();
  EXPECT_TRUE(divider_widget);
  EXPECT_TRUE(divider_widget->IsVisible());
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(), divider);

  // Verify that the divider is invisible in Overview.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  EXPECT_FALSE(divider_widget->IsVisible());

  // Verify that the divider is visible again on Overview exit.
  ToggleOverview();
  ASSERT_FALSE(IsInOverviewSession());
  EXPECT_TRUE(divider_widget->IsVisible());
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(), divider);
}

// Tests that the desk removal with only one active desk by keyboard shortcut
// `Search + Shift + -` will not be successful, the Snap Group will remain on
// the active desk.
TEST_F(SnapGroupDesksTest, DeskRemovalWithOneAciveDesk) {
  auto* desks_controller = DesksController::Get();
  ASSERT_EQ(1u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  // Use `Search + Shift + -` to attempt to remove `desk0`.
  PressAndReleaseKey(ui::VKEY_OEM_MINUS,
                     ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);

  // Verify that both `w0` and `w1` will still be on `desk0`.
  EXPECT_EQ(1u, desks_controller->desks().size());
  EXPECT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  EXPECT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));
  EXPECT_TRUE(snap_group);
  auto* divider = snap_group->snap_group_divider();
  auto* divider_widget = divider->divider_widget();
  EXPECT_TRUE(divider_widget);
  EXPECT_TRUE(divider_widget->IsVisible());
  UnionBoundsEqualToWorkAreaBounds(w0.get(), w1.get(), divider);
}

// Test that merging a desk with a Snap Group into another desk doesn't cause
// crash and correctly moves all windows to the destination desk.
TEST_F(SnapGroupDesksTest, DesksMerge) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  ASSERT_FALSE(desk0->is_active());

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w1.get()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  // Merge `desk1` into `desk0`.
  RemoveDesk(desk1, DeskCloseType::kCombineDesks);

  // Verify that windows in the Snap Group are properly moved to the new desk.
  EXPECT_EQ(1u, desks_controller->desks().size());
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));
}

// Tests that create one Snap Group per desk and then switching between desks,
// the windows in the Snap Group remain visible. See regression at
// http://b/335477702.
TEST_F(SnapGroupDesksTest, OneSnapGroupOnEachDesk) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  // Create `snap_group0` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group0 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group0);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  ASSERT_FALSE(desk0->is_active());

  // Create `snap_group1` on `desk1`.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group1 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group1);
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w2.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w3.get()));

  // Activate `desk0` and verify that both windows in `snap_group0` are visible.
  ActivateDesk(desk0);
  EXPECT_TRUE(w0->IsVisible());
  EXPECT_TRUE(w1->IsVisible());

  // Activate `desk1` and verify that both windows in `snap_group1` are visible.
  ActivateDesk(desk1);
  EXPECT_TRUE(w2->IsVisible());
  EXPECT_TRUE(w3->IsVisible());
}

// Verify that existing snap groups are hidden in partial overview mode only if
// they are located on the currently active desktop.
TEST_F(SnapGroupDesksTest, OnlyHideSnapGroupOnActiveDesk) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  // Create `snap_group0` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group0 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group0);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  // Activate `desk1` and start partial Overview on `desk1`.
  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  ASSERT_FALSE(desk0->is_active());

  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w2.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  VerifySplitViewOverviewSession(w2.get());

  // Verify that the target visibility of `w0` and `w1` are not affected.
  EXPECT_TRUE(w0->TargetVisibility());
  EXPECT_TRUE(w1->TargetVisibility());
}

// Tests that accessing the saved desks library after creating a Snap Group does
// not result in a crash, and the Snap Group is successfully restored upon
// exiting overview mode. See regression at http://b/335301800.
TEST_F(SnapGroupDesksTest, SaveDeskForSnapGroupWithAnotherSavedDeskOld) {
  base::test::ScopedFeatureList disable;
  disable.InitAndDisableFeature(features::kSavedDeskUiRevamp);

  OverviewController* overview_controller = OverviewController::Get();

  // Explicitly disable `disable_app_id_check_for_saved_desks_` otherwise "Save
  // desk for later" button will be disabled.
  base::AutoReset<bool> disable_app_id_check =
      overview_controller->SetDisableAppIdCheckForTests();

  // Create `w0` and save `w0` in a saved desk by activing "Save desk for later"
  // button in Overview.
  std::unique_ptr<aura::Window> w0(
      CreateAppWindow(gfx::Rect(10, 10, 500, 300)));

  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  OverviewSession* overview_session = overview_controller->overview_session();
  ASSERT_TRUE(overview_session);

  auto* root_window = Shell::GetPrimaryRootWindow();
  OverviewGrid* overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  ASSERT_EQ(1u, overview_grid->item_list().size());

  auto* save_for_later_button = overview_grid->GetSaveDeskForLaterButton();
  ASSERT_TRUE(save_for_later_button);
  base::RunLoop().RunUntilIdle();
  LeftClickOn(save_for_later_button);

  auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);

  auto* library_button = desks_bar_view->library_button();
  ASSERT_TRUE(library_button);

  overview_controller->EndOverview(OverviewEndAction::kOverviewButton);

  // Create a Snap Group and enter Overview again, click on the library button
  // on the virtual desks bar and verify that there is no crash.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  ASSERT_TRUE(overview_session);

  overview_grid = GetOverviewGridForRoot(root_window);
  desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);

  library_button = desks_bar_view->library_button();
  ASSERT_TRUE(library_button);

  auto* overview_group_item = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(overview_group_item);
  ASSERT_FALSE(GetTopmostSnapGroupDivider()->divider_widget()->IsVisible());

  const auto cached_group_item_bounds = overview_group_item->target_bounds();

  LeftClickOn(library_button);

  // Click the point outside of `cached_group_item_bounds` will exit Overview
  // and bring back the Snap Group.
  const gfx::Point click_point = gfx::ToRoundedPoint(
      cached_group_item_bounds.bottom_right() + gfx::Vector2d(20, 0));
  event_generator->MoveMouseTo(click_point);

  event_generator->ClickLeftButton();
  EXPECT_FALSE(IsInOverviewSession());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that accessing the saved desks library after creating a Snap Group does
// not result in a crash, and the Snap Group is successfully restored upon
// exiting overview mode. See regression at http://b/335301800.
TEST_F(SnapGroupDesksTest, SaveDeskForSnapGroupWithAnotherSavedDesk) {
  saved_desk_test_helper()->WaitForDeskModels();
  base::test::ScopedFeatureList enable{features::kSavedDeskUiRevamp};

  OverviewController* overview_controller = OverviewController::Get();
  // Explicitly disable `disable_app_id_check_for_saved_desks_` otherwise "Save
  // desk for later" context menu item will be disabled.
  base::AutoReset<bool> disable_app_id_check =
      overview_controller->SetDisableAppIdCheckForTests();

  // Create a window and save it in a saved desk by clicking the "Save desk for
  // later" menu item in Overview. Release ownership as it will be destroyed
  // when saving it.
  CreateAppWindow(gfx::Rect(500, 300)).release();

  // Open Overview and then click the "Save desk for later" menu item. Verify
  // that it has saved a desk by checking for the library button.
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  views::MenuItemView* menu_item =
      DesksTestApi::OpenDeskContextMenuAndGetMenuItem(
          Shell::GetPrimaryRootWindow(), DeskBarViewBase::Type::kOverview,
          /*index=*/0u, DeskActionContextMenu::CommandId::kSaveForLater);
  LeftClickOn(menu_item);
  // We have to wait one extra time for the closing windows. See
  // `SavedDeskTest::OpenOverviewAndSaveDeskForLater()`.
  WaitForSavedDeskUI();
  WaitForSavedDeskUI();
  ASSERT_TRUE(GetLibraryButton());
  overview_controller->EndOverview(OverviewEndAction::kOverviewButton);

  // Create a Snap Group and enter Overview again, click on the library button
  // on the virtual desks bar and verify that there is no crash.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  auto* group_item = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(group_item);
  ASSERT_FALSE(GetTopmostSnapGroupDivider()->divider_widget()->IsVisible());
  const gfx::RectF cached_group_item_bounds = group_item->target_bounds();
  auto* library_button = GetLibraryButton();
  LeftClickOn(library_button);

  // Click the point outside of `cached_group_item_bounds` will exit Overview
  // and bring back the Snap Group.
  const gfx::Point click_point = gfx::ToRoundedPoint(
      cached_group_item_bounds.bottom_right() + gfx::Vector2d(20, 0));
  event_generator->MoveMouseTo(click_point);
  event_generator->ClickLeftButton();
  EXPECT_FALSE(IsInOverviewSession());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Verify that Snap Group will be broken when setting a window that belongs to a
// Snap Group to be visible on all workspaces.
TEST_F(SnapGroupDesksTest, MoveToAllDesksToBreakTheGroup) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  // Create `snap_group` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  auto* window_widget0 = views::Widget::GetWidgetForNativeView(w0.get());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  // Move `w0` to all desks.
  window_widget0->SetVisibleOnAllWorkspaces(true);

  // Verify that `snap_group` will be broken.
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w0.get(), w1.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w0.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
}

// Test to verify that moving a Snap Group to all desks and removing it from its
// original desk doesn't cause a crash with one Snap Group per desk. See
// http://b/344982754 for more details.
TEST_F(SnapGroupDesksTest, DeskRemovalAfterMovingSnapGroupToAllDesks) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());
  ASSERT_FALSE(desk1->is_active());

  // Create `snap_group0` on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  auto* window_widget0 = views::Widget::GetWidgetForNativeView(w0.get());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);

  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  SnapGroup* snap_group0 =
      snap_group_controller->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group0);
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w0.get()));
  ASSERT_EQ(desk0, desks_util::GetDeskForContext(w1.get()));

  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  ASSERT_FALSE(desk0->is_active());

  // Create `snap_group1` on `desk1`.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group1 =
      snap_group_controller->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group1);
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w2.get()));
  ASSERT_EQ(desk1, desks_util::GetDeskForContext(w3.get()));

  ActivateDesk(desk0);

  // Move `snap_group0` to all desks will break the Snap Group.
  window_widget0->SetVisibleOnAllWorkspaces(true);
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w0.get(), w1.get()));

  // Pre-release ownership of `w0` and `w1` using `release()`. This is crucial
  // to avoid double-freeing memory. When unique_ptr goes out of scope, its
  // destructor will attempt to deallocate the owned memory. Since CloseAll will
  // already handle the window destruction, leaving the unique_ptrs to manage
  // the memory would lead to a second deallocation attempt on the same address,
  // resulting in crash.
  w0.release();
  w1.release();

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  // Remove `desk0` and verify that there will be no CHECK crash.
  RemoveDesk(desk0, DeskCloseType::kCloseAllWindowsAndWait);
  ASSERT_TRUE(desk0->is_desk_being_removed());

  base::RunLoop().RunUntilIdle();
}

// -----------------------------------------------------------------------------
// SnapGroupWindowCycleTest:

class SnapGroupWindowCycleTest : public SnapGroupTest {
 public:
  SnapGroupWindowCycleTest() {
    WindowCycleList::SetDisableInitialDelayForTesting(true);
  }

  ~SnapGroupWindowCycleTest() override = default;

  void AltTabNTimes(int n) {
    WindowCycleController* window_cycle_controller =
        Shell::Get()->window_cycle_controller();

    auto* event_generator = GetEventGenerator();

    for (int i = 0; i < n; i++) {
      event_generator->PressKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
      event_generator->ReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
      EXPECT_TRUE(window_cycle_controller->IsCycling());
    }

    event_generator->ReleaseKey(ui::VKEY_MENU, ui::EF_NONE);
    EXPECT_FALSE(window_cycle_controller->IsCycling());
  }
};

// Tests that the window list is reordered when there is snap group. The two
// windows will be adjacent with each other with physically left/top snapped
// window put before physically right/bottom snapped window.
TEST_F(SnapGroupWindowCycleTest, WindowReorderInAltTabInPrimaryOrientation) {
  std::unique_ptr<aura::Window> window0(CreateTestWindowInShellWithId(0));
  window0->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithId(1));
  window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithId(2));
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  wm::ActivateWindow(window2.get());
  // Initial window activation order: window2, [window1, window0].
  ASSERT_TRUE(wm::IsActiveWindow(window2.get()));

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/1);

  const auto& windows =
      window_cycle_controller->window_cycle_list()->windows_for_testing();

  // Test that the two windows in a snap group are reordered to be adjacent
  // with each other to reflect the window layout with the revised order as :
  // window2, [window0, window1].
  ASSERT_EQ(windows.size(), 3u);
  EXPECT_EQ(windows.at(0), window2.get());
  EXPECT_EQ(windows.at(1), window0.get());
  EXPECT_EQ(windows.at(2), window1.get());
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window0.get()));

  // With the activation of `window1`, `window0` will be inserted right before
  // `window1`.
  // The new window cycle list order as: [window0, window1], window2. Cycle
  // twice to focus on `window2`.
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/2);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window2.get()));
}

// Tests that the number of views to be cycled through inside the mirror
// container view of window cycle view will be the number of free-form windows
// plus snap groups.
TEST_F(SnapGroupWindowCycleTest, WindowCycleViewTest) {
  std::unique_ptr<aura::Window> window0(CreateTestWindowInShellWithId(0));
  window0->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithId(1));
  window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithId(2));
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  const auto& windows = window_cycle_list->windows_for_testing();
  EXPECT_EQ(windows.size(), 3u);

  const WindowCycleView* cycle_view = window_cycle_list->cycle_view();
  ASSERT_TRUE(cycle_view);
  EXPECT_EQ(cycle_view->mirror_container_for_testing()->children().size(), 2u);
  CompleteWindowCycling();
}

// Tests that on window that belongs to a snap group destroying while cycling
// the window list with Alt + Tab, there will be no crash. The corresponding
// child mini view hosted by the group container view will be destroyed, the
// group container view will host the other child mini view.
TEST_F(SnapGroupWindowCycleTest, WindowInSnapGroupDestructionInAltTab) {
  std::unique_ptr<aura::Window> window0(CreateTestWindowInShellWithId(0));
  window0->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithId(1));
  window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithId(2));
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  const auto& windows = window_cycle_list->windows_for_testing();
  EXPECT_EQ(windows.size(), 3u);

  const WindowCycleView* cycle_view = window_cycle_list->cycle_view();
  ASSERT_TRUE(cycle_view);
  // Verify that the number of child views hosted by mirror container is two at
  // the beginning.
  EXPECT_EQ(cycle_view->mirror_container_for_testing()->children().size(), 2u);

  // Destroy `window0` which belongs to a snap group.
  window0.reset();
  // Verify that we should still be cycling.
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const auto* updated_window_cycle_list =
      window_cycle_controller->window_cycle_list();
  const auto& updated_windows =
      updated_window_cycle_list->windows_for_testing();
  // Verify that the updated windows list size decreased.
  EXPECT_EQ(updated_windows.size(), 2u);

  // Verify that the number of child views hosted by mirror container will still
  // be two.
  EXPECT_EQ(cycle_view->mirror_container_for_testing()->children().size(), 2u);
}

// Tests and verifies the steps it takes to focus on a window cycle item by
// tabbing and reverse tabbing. The focused item will be activated upon
// completion of window cycling.
TEST_F(SnapGroupWindowCycleTest, SteppingInWindowCycleView) {
  std::unique_ptr<aura::Window> window3 =
      CreateAppWindow(gfx::Rect(300, 300), chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2 =
      CreateAppWindow(gfx::Rect(200, 200), chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window1 =
      CreateAppWindow(gfx::Rect(100, 100), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window0 =
      CreateAppWindow(gfx::Rect(10, 10), chromeos::AppType::BROWSER);

  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(wm::IsActiveWindow(window1.get()));
  WindowState::Get(window3.get())->Activate();
  EXPECT_TRUE(wm::IsActiveWindow(window3.get()));

  // Window cycle list:
  // window3, [window0, window1], window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/2);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window1.get()));

  // Window cycle list:
  // [window0, window1], window3, window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/1);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window0.get()));

  // Window cycle list:
  // [window0, window1], window3, window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window2.get()));

  // Window cycle list:
  // window2, [window0, window1], window3
  CycleWindow(WindowCyclingDirection::kBackward, /*steps=*/1);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window3.get()));
}

// Tests that using 'Alt + Tab' for quick switch correctly cycles focus between
// two snapped windows within a Snap Group, without showing the window cycling
// UI.
TEST_F(SnapGroupWindowCycleTest, QuickSwitch) {
  WindowCycleList::SetDisableInitialDelayForTesting(false);

  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(wm::IsActiveWindow(w1.get()));

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();

  // Press 'Alt + Tab' keyboard shortcut to trigger window cycling, verify that
  // the focus is switched to `w0` in the Snap Group.
  auto* event_generator = GetEventGenerator();
  event_generator->PressKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  event_generator->ReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const auto* window_cycle_list0 = window_cycle_controller->window_cycle_list();
  ASSERT_TRUE(window_cycle_list0);
  EXPECT_FALSE(window_cycle_list0->cycle_view());
  event_generator->ReleaseKey(ui::VKEY_MENU, ui::EF_NONE);
  EXPECT_TRUE(wm::IsActiveWindow(w0.get()));

  // Press 'Alt + Tab' keyboard shortcut again to trigger window cycling ,
  // verify that the focus is switched back to `w1` in the Snap Group.
  event_generator->PressKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  event_generator->ReleaseKey(ui::VKEY_TAB, ui::EF_ALT_DOWN);
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const auto* window_cycle_list1 = window_cycle_controller->window_cycle_list();
  ASSERT_TRUE(window_cycle_list1);
  EXPECT_FALSE(window_cycle_list1->cycle_view());
  event_generator->ReleaseKey(ui::VKEY_MENU, ui::EF_NONE);
  EXPECT_TRUE(wm::IsActiveWindow(w1.get()));
}

// Tests that window cycling correctly navigates across multiple Snap Groups on
// different virtual desks. Upon completing a window cycle, the active desk is
// switched to the one containing the activated window.
TEST_F(SnapGroupWindowCycleTest, AllDesksWindowCycling) {
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  // Create 1st Snap Group on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group1 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group1);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  // Create 2nd Snap Group on `desk1`.
  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroup* snap_group2 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group2);
  ASSERT_EQ(desks_util::GetDeskForContext(w2.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(w3.get()), desk1);

  EXPECT_TRUE(wm::IsActiveWindow(w3.get()));

  // Initial window cycle UI for "All desks" is as follows:
  //
  //        desk1           |        desk0
  //                        |
  //     +------+------+    |    +------+------+
  //     |      |      |    |    |      |      |
  //     |  w2  | [w3] |    |    |  w0  |  w1  |
  //     |      |      |    |    |      |      |
  //     +------+------+    |    +------+------+

  // Press 'Alt + Tab' 3 times to activate `w0`(cycling sequence:
  // `w2`->`w3`->`w0`). Verify that the active desk is switched to `desk0`
  // correspondingly.
  AltTabNTimes(3);

  EXPECT_TRUE(wm::IsActiveWindow(w0.get()));
  DeskSwitchAnimationWaiter().Wait();
  EXPECT_TRUE(desk0->is_active());

  // Updated window cycle UI for "All desks" is as follows:
  //
  //        desk0           |        desk1
  //                        |
  //     +------+------+    |    +------+------+
  //     |      |      |    |    |      |      |
  //     | [w0] |  w1  |    |    |  w2  |  w3  |
  //     |      |      |    |    |      |      |
  //     +------+------+    |    +------+------+

  // Press 'Alt + Tab' 2 times to activate `w2`(cycling sequence: `w1`->`w2`).
  // Verify that the active desk is switched to `desk1` correspondingly.
  AltTabNTimes(2);

  EXPECT_TRUE(wm::IsActiveWindow(w2.get()));
  DeskSwitchAnimationWaiter().Wait();
  EXPECT_TRUE(desk1->is_active());
}

// Verifies that window cycling through Snap Groups on multiple virtual desks
// only activates windows present on the currently active desk. Windows on
// inactive desktops will not be cycled through or brought into focus.
TEST_F(SnapGroupWindowCycleTest, PerDeskWindowCycling) {
  PrefService* active_user_prefs =
      Shell::Get()->session_controller()->GetActivePrefService();
  DCHECK(active_user_prefs);
  active_user_prefs->SetBoolean(prefs::kAltTabPerDesk, true);

  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  // Create 1st Snap Group on `desk0`.
  std::unique_ptr<aura::Window> w0(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group1 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group1);
  ASSERT_EQ(desks_util::GetDeskForContext(w0.get()), desk0);
  ASSERT_EQ(desks_util::GetDeskForContext(w1.get()), desk0);

  // Create 2nd Snap Group on `desk1`.
  ActivateDesk(desk1);
  ASSERT_TRUE(desk1->is_active());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  SnapGroup* snap_group2 =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w0.get());
  ASSERT_TRUE(snap_group2);
  ASSERT_EQ(desks_util::GetDeskForContext(w2.get()), desk1);
  ASSERT_EQ(desks_util::GetDeskForContext(w3.get()), desk1);

  EXPECT_TRUE(wm::IsActiveWindow(w3.get()));

  // Tests per-desk focus cycling on `desk1`.
  // Press 'Alt + Tab' 3 times to activate `w2`(cycling sequence:
  // `w2`->`w3`->`w2`). Verify that the active desk will not be modified.
  AltTabNTimes(3);

  EXPECT_TRUE(wm::IsActiveWindow(w2.get()));
  EXPECT_FALSE(desks_controller->AreDesksBeingModified());

  // Tests per-desk focus cycling on `desk0`.
  ActivateDesk(desk0);
  ASSERT_TRUE(desk0->is_active());

  // Press 'Alt + Tab' 5 times to activate `w2`(cycling sequence:
  // `w0`->`w1`->`w0`->`w1`->`w0`). Verify that the active desk will not be
  // modified.
  AltTabNTimes(5);

  EXPECT_TRUE(wm::IsActiveWindow(w0.get()));
  EXPECT_FALSE(desks_controller->AreDesksBeingModified());
}

// Tests that the exposed rounded corners of the cycling items are rounded
// corners. The visuals will be refreshed on window destruction that belongs to
// a snap group.
TEST_F(SnapGroupWindowCycleTest, WindowCycleItemRoundedCorners) {
  std::unique_ptr<aura::Window> window0 =
      CreateAppWindow(gfx::Rect(100, 200), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window1 =
      CreateAppWindow(gfx::Rect(200, 300), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window2 =
      CreateAppWindow(gfx::Rect(300, 400), chromeos::AppType::BROWSER);
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  const auto* cycle_view = window_cycle_list->cycle_view();
  auto& cycle_item_views = cycle_view->cycle_views_for_testing();
  ASSERT_EQ(cycle_item_views.size(), 2u);
  for (ash::WindowMiniViewBase* cycle_item_view : cycle_item_views) {
    EXPECT_EQ(cycle_item_view->GetRoundedCorners(),
              gfx::RoundedCornersF(kWindowMiniViewCornerRadius));
  }

  // Destroy `window0` which belongs to a snap group while cycling.
  window0.reset();
  auto& new_cycle_item_views = cycle_view->cycle_views_for_testing();
  EXPECT_EQ(new_cycle_item_views.size(), 2u);

  // Verify that the visuals of the cycling items will be refreshed so that the
  // exposed corners will be rounded corners.
  for (ash::WindowMiniViewBase* cycle_item_view : new_cycle_item_views) {
    EXPECT_EQ(cycle_item_view->GetRoundedCorners(),
              gfx::RoundedCornersF(kWindowMiniViewCornerRadius));
  }
  CompleteWindowCycling();
}

TEST_F(SnapGroupWindowCycleTest, WindowCycleItemRoundedCornersInPortait) {
  UpdateDisplay("600x900");

  std::unique_ptr<aura::Window> window0 =
      CreateAppWindow(gfx::Rect(100, 200), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window1 =
      CreateAppWindow(gfx::Rect(200, 300), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window2 =
      CreateAppWindow(gfx::Rect(300, 400), chromeos::AppType::BROWSER);
  SnapTwoTestWindows(window0.get(), window1.get(), /*horizontal=*/false,
                     GetEventGenerator());

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  EXPECT_TRUE(window_cycle_controller->IsCycling());
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  const auto* cycle_view = window_cycle_list->cycle_view();
  auto& cycle_item_views = cycle_view->cycle_views_for_testing();
  ASSERT_EQ(cycle_item_views.size(), 2u);
  for (ash::WindowMiniViewBase* cycle_item_view : cycle_item_views) {
    EXPECT_EQ(cycle_item_view->GetRoundedCorners(),
              gfx::RoundedCornersF(kWindowMiniViewCornerRadius));
  }
}

// Tests that in non-primary display orientations, the visuals of the Snap Group
// item within the `Alt + Tab` cycle view accurately represent the actual layout
// of the windows in the group, and that stepping through the windows in the
// Alt + Tab also follows the correct layout order. See http://b/339023083 for
// more details about the issue.
TEST_F(SnapGroupWindowCycleTest,
       WindowCycleItemForNonPrimaryScreenOrientation) {
  // Update display to be in non-primary portrait mode.
  UpdateDisplay("1200x900/r");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(1U, displays.size());
  ASSERT_EQ(chromeos::OrientationType::kPortraitSecondary,
            chromeos::GetDisplayCurrentOrientation(displays[0]));

  std::unique_ptr<aura::Window> window2 =
      CreateAppWindow(gfx::Rect(200, 200), chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window1 =
      CreateAppWindow(gfx::Rect(100, 100), chromeos::AppType::BROWSER);
  std::unique_ptr<aura::Window> window0 =
      CreateAppWindow(gfx::Rect(10, 10), chromeos::AppType::BROWSER);

  // Drag `window0` to the **top** of the screen to snap it into the
  // **secondary** position, as the display is currently oriented in secondary
  // portrait mode.
  SnapOneTestWindow(window0.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_TRUE(IsInOverviewSession());

  SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, GetEventGenerator());
  GetEventGenerator()->PressKey(ui::VKEY_RETURN, /*flags=*/0);
  ASSERT_TRUE(SnapGroupController::Get()->AreWindowsInSnapGroup(window0.get(),
                                                                window1.get()));

  // With non-primary layout, this is the layout of `window0` and `window1`.
  //              +-----------+
  //              |           |
  //              |    w0     |
  //              |           |
  //              |-----------|
  //              |           |
  //              |    w1     |
  //              |           |
  //              +-----------+

  // Verify the windows bounds i.e. `window0` is on top (secondary snapped) and
  // `window1` in on bottom (primary snapped).
  gfx::Rect work_area = GetWorkAreaBoundsForWindow(window0.get());
  const gfx::Rect divider_bounds =
      GetTopmostSnapGroupDivider()->divider_widget()->GetWindowBoundsInScreen();
  EXPECT_EQ(gfx::Rect(work_area.x(), work_area.y(), work_area.width(),
                      divider_bounds.y()),
            window0->GetBoundsInScreen());
  EXPECT_EQ(gfx::Rect(work_area.x(), divider_bounds.bottom(), work_area.width(),
                      work_area.height() - divider_bounds.bottom()),
            window1->GetBoundsInScreen());

  EXPECT_TRUE(wm::IsActiveWindow(window1.get()));

  // Create `window3` and start testing the stepping.
  std::unique_ptr<aura::Window> window3 =
      CreateAppWindow(gfx::Rect(300, 300), chromeos::AppType::CHROME_APP);
  EXPECT_TRUE(wm::IsActiveWindow(window3.get()));

  // Window cycle list:
  // window3, [window0, window1], window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/2);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window1.get()));

  // Window cycle list:
  // [window0, window1], window3, window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/1);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window0.get()));

  // Window cycle list:
  // [window0, window1], window3, window2
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/3);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window2.get()));

  // Window cycle list:
  // window2, [window0, window1], window3
  CycleWindow(WindowCyclingDirection::kBackward, /*steps=*/1);
  CompleteWindowCycling();
  EXPECT_TRUE(wm::IsActiveWindow(window3.get()));
}

// Tests that two windows in a snap group is allowed to be shown as group item
// view only if both of them belong to the same app as the mru window. If only
// one window belongs to the app, the representation of the window will be shown
// as the individual window cycle item view.
TEST_F(SnapGroupWindowCycleTest, SameAppWindowCycle) {
  struct app_id_pair {
    const char* trace_message;
    const std::string app_id_2;
    const std::string app_id_3;
    const size_t windows_size;
    const size_t cycle_views_count;
  } kTestCases[]{
      {/*trace_message=*/"Windows in snap group with same app id",
       /*app_id_2=*/"A", /*app_id_3=*/"A", /*windows_size=*/4u,
       /*cycle_views_count=*/3u},
      {/*trace_message=*/"Windows in snap group with different app ids",
       /*app_id_2=*/"A", /*app_id_3=*/"B", /*windows_size=*/3u,
       /*cycle_views_count=*/3u},
  };

  std::unique_ptr<aura::Window> w0(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w1(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w2(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w3(CreateTestWindowWithAppID(std::string("A")));
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true,
                     GetEventGenerator());
  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  for (const auto& test_case : kTestCases) {
    w2->SetProperty(kAppIDKey, std::move(test_case.app_id_2));
    w3->SetProperty(kAppIDKey, std::move(test_case.app_id_3));

    wm::ActivateWindow(w2.get());
    ASSERT_TRUE(wm::IsActiveWindow(w2.get()));

    // Simulate pressing Alt + Backtick to trigger the same app cycling.
    auto* event_generator = GetEventGenerator();
    event_generator->PressKey(ui::VKEY_MENU, ui::EF_NONE);
    event_generator->PressAndReleaseKey(ui::VKEY_OEM_3, ui::EF_ALT_DOWN);

    const auto* window_cycle_list =
        window_cycle_controller->window_cycle_list();
    ASSERT_TRUE(window_cycle_list->same_app_only());

    // Verify the number of windows for the cycling.
    const auto& windows = window_cycle_list->windows_for_testing();
    EXPECT_EQ(windows.size(), test_case.windows_size);
    EXPECT_TRUE(window_cycle_controller->IsCycling());
    const auto* cycle_view = window_cycle_list->cycle_view();
    ASSERT_TRUE(cycle_view);

    // Verify the number of cycle views.
    auto& cycle_item_views = cycle_view->cycle_views_for_testing();
    EXPECT_EQ(cycle_item_views.size(), test_case.cycle_views_count);
    event_generator->ReleaseKey(ui::VKEY_MENU, ui::EF_NONE);
  }
}

// Tests and verifies that if one of the window in a snap group gets destroyed
// while doing same app window cycling the corresponding window cycle item view
// will be properly removed and re-configured with no crash.
TEST_F(SnapGroupWindowCycleTest, WindowDestructionDuringSameAppWindowCycle) {
  std::unique_ptr<aura::Window> w0(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w1(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w2(CreateTestWindowWithAppID(std::string("A")));
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Simulate pressing Alt + Backtick to trigger the same app cycling.
  auto* event_generator = GetEventGenerator();
  event_generator->PressKey(ui::VKEY_MENU, ui::EF_NONE);
  event_generator->PressAndReleaseKey(ui::VKEY_OEM_3, ui::EF_ALT_DOWN);

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  ASSERT_TRUE(window_cycle_list->same_app_only());
  const auto* cycle_view = window_cycle_list->cycle_view();
  ASSERT_TRUE(cycle_view);
  const auto& windows = window_cycle_list->windows_for_testing();
  EXPECT_EQ(windows.size(), 3u);
  w0.reset();

  // After the window destruction, the window cycle view is still available.
  ASSERT_TRUE(cycle_view);
  const auto& updated_windows = window_cycle_list->windows_for_testing();
  EXPECT_EQ(updated_windows.size(), 2u);
  CompleteWindowCycling();
}

// Tests that if a snap group is at the beginning of a window cycling list, the
// mru window will depend on the mru window between the two windows in the snap
// group, since the windows are reordered so that it reflects the actual window
// layout.
TEST_F(SnapGroupWindowCycleTest, MruWindowForSameApp) {
  // Generate 5 windows with 3 of them from app A and 2 of them from app B.
  std::unique_ptr<aura::Window> w0(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w1(CreateTestWindowWithAppID(std::string("B")));
  std::unique_ptr<aura::Window> w2(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w3(CreateTestWindowWithAppID(std::string("A")));
  std::unique_ptr<aura::Window> w4(CreateTestWindowWithAppID(std::string("B")));
  SnapTwoTestWindows(w0.get(), w1.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Specifically activate the secondary snapped window with app type B.
  wm::ActivateWindow(w1.get());

  // Simulate pressing Alt + Backtick to trigger the same app cycling.
  auto* event_generator = GetEventGenerator();
  event_generator->PressKey(ui::VKEY_MENU, ui::EF_NONE);
  event_generator->PressAndReleaseKey(ui::VKEY_OEM_3, ui::EF_ALT_DOWN);

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  const auto* window_cycle_list = window_cycle_controller->window_cycle_list();
  ASSERT_TRUE(window_cycle_list->same_app_only());
  const auto& windows = window_cycle_list->windows_for_testing();

  // Verify that the windows in the list that are been cycled all belong to app
  // B.
  EXPECT_EQ(windows.size(), 2u);
  CompleteWindowCycling();
}

// -----------------------------------------------------------------------------
// SnapGroupTabletConversionTest:

using SnapGroupTabletConversionTest = SnapGroupTest;

// Tests that after creating a snap group in clamshell, transition to tablet
// mode won't crash (b/288179725).
TEST_F(SnapGroupTabletConversionTest, NoCrashWhenRemovingGroupInTabletMode) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  SwitchToTabletMode();

  // Close w2. Test that the group is destroyed but we are still in split view.
  w2.reset();
  SnapGroupController* snap_group_controller =
      Shell::Get()->snap_group_controller();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w2.get()));
  EXPECT_EQ(GetSplitViewController()->primary_window(), w1.get());
  EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
}

// Tests that one snap group in clamshell will be converted to windows in tablet
// split view. When converted back to clamshell, the snap group will be
// restored.
TEST_F(SnapGroupTabletConversionTest,
       ClamshellTabletTransitionWithOneSnapGroup) {
  std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithId(0));
  window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithId(1));
  window2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  SnapTwoTestWindows(window1.get(), window2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  UnionBoundsEqualToWorkAreaBounds(window1.get(), window2.get(),
                                   GetTopmostSnapGroupDivider());

  SwitchToTabletMode();
  EXPECT_FALSE(GetTopmostSnapGroupDivider());
  EXPECT_TRUE(GetSplitViewDivider()->divider_widget());
  // The snap group is removed in tablet mode.
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_FALSE(
      snap_group_controller->GetSnapGroupForGivenWindow(window1.get()));
  EXPECT_EQ(window1.get(), GetSplitViewController()->primary_window());
  EXPECT_EQ(window2.get(), GetSplitViewController()->secondary_window());
  UnionBoundsEqualToWorkAreaBounds(window1.get(), window2.get(),
                                   GetSplitViewDivider());
  EXPECT_NEAR(chromeos::kDefaultSnapRatio,
              *WindowState::Get(window1.get())->snap_ratio(), 0.05);
  EXPECT_NEAR(chromeos::kDefaultSnapRatio,
              *WindowState::Get(window2.get())->snap_ratio(), 0.05);

  ExitTabletMode();
  EXPECT_TRUE(SnapGroupController::Get()->AreWindowsInSnapGroup(window1.get(),
                                                                window2.get()));
  EXPECT_NEAR(chromeos::kDefaultSnapRatio,
              *WindowState::Get(window1.get())->snap_ratio(), 0.05);
  EXPECT_NEAR(chromeos::kDefaultSnapRatio,
              *WindowState::Get(window2.get())->snap_ratio(), 0.05);
  UnionBoundsEqualToWorkAreaBounds(window1.get(), window2.get(),
                                   GetTopmostSnapGroupDivider());
  EXPECT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
}

// Tests that when converting to tablet mode with split view divider at an
// arbitrary location, the bounds of the two windows and the divider will be
// updated such that the snap ratio of the layout is one of the fixed snap
// ratios.
TEST_F(SnapGroupTabletConversionTest,
       ClamshellTabletTransitionGetClosestFixedRatio) {
  UpdateDisplay("900x600");
  std::unique_ptr<aura::Window> window1(CreateTestWindowInShellWithId(0));
  window1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> window2(CreateTestWindowInShellWithId(1));
  window2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(window1.get(), window2.get(), /*horizontal=*/true,
                     event_generator);
  ASSERT_TRUE(GetTopmostSnapGroupDivider()->divider_widget());
  EXPECT_EQ(*WindowState::Get(window1.get())->snap_ratio(),
            chromeos::kDefaultSnapRatio);

  // Build test cases to be used for divider dragging, with expected fixed ratio
  // and corresponding pixels shown in the ASCII diagram below:
  //   ┌────────────────┬────────────────┐
  //   │                │                │
  //   │                │                │
  //   │                │                │
  //   │                │                │
  //   │                │                │
  //   │                │                │
  //   │                │                │
  //   └─────────┬──────┼───────┬────────┘
  //             │      │       │
  // ratio:     1/3    1/2     2/3
  // pixel:     300    450     600      900

  struct {
    int distance_delta;
    float expected_snap_ratio;
  } kTestCases[]{{/*distance_delta=*/-200, chromeos::kOneThirdSnapRatio},
                 {/*distance_delta=*/400, chromeos::kTwoThirdSnapRatio},
                 {/*distance_delta=*/-180, chromeos::kDefaultSnapRatio}};

  const gfx::Rect work_area_bounds_in_screen =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          GetSplitViewController()->root_window()->GetChildById(
              desks_util::GetActiveDeskContainerId()));
  for (const auto test_case : kTestCases) {
    event_generator->set_current_screen_location(
        GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
    event_generator->DragMouseBy(test_case.distance_delta, 0);
    GetTopmostSnapGroupDivider()->EndResizeWithDivider(
        event_generator->current_screen_location());
    SwitchToTabletMode();
    EXPECT_TRUE(GetSplitViewDivider() && !GetTopmostSnapGroupDivider());
    const auto current_divider_position =
        GetSplitViewDividerBoundsInScreen().x();

    // We need to take into consideration of the variation introduced by the
    // divider shorter side length when calculating using snap ratio, i.e.
    // `kSplitviewDividerShortSideLength / 2`.
    const auto expected_divider_position = std::round(
        work_area_bounds_in_screen.width() * test_case.expected_snap_ratio -
        kSplitviewDividerShortSideLength / 2);

    // Verifies that the bounds of the windows and divider are updated correctly
    // such that snap ratio in the new window layout is expected.
    EXPECT_NEAR(current_divider_position, expected_divider_position,
                /*abs_error=*/1);
    EXPECT_NEAR(float(window1->GetBoundsInScreen().width()) /
                    work_area_bounds_in_screen.width(),
                test_case.expected_snap_ratio, /*abs_error=*/1);
    ExitTabletMode();
  }
}

// Verify that entering tablet mode with Snap Group in Overview results in the
// Snap Group being represented by two separate items in Overview. This change
// should be maintained when switching back to clamshell mode. See
// http://b/343803517 for more details.
TEST_F(SnapGroupTabletConversionTest, TransitionToTabletInOverview) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  // Verify that there is one `OverviewGroupItem` initially in clamshell mode.
  EXPECT_EQ(1u, overview_grid->item_list().size());

  // Upon switching to tablet mode, the `OverviewGroupItem` is removed and
  // replaced with two separate items, there is no overlap in their bounds.
  SwitchToTabletMode();
  EXPECT_EQ(2u, overview_grid->item_list().size());

  OverviewItemBase* overview_item1 = GetOverviewItemForWindow(w1.get());
  OverviewItemBase* overview_item2 = GetOverviewItemForWindow(w2.get());
  EXPECT_NE(overview_item1, overview_item2);
  const gfx::RectF overview_item1_bounds_tablet =
      overview_item1->target_bounds();
  const gfx::RectF overview_item2_bounds_tablet =
      overview_item2->target_bounds();
  EXPECT_FALSE(
      overview_item1_bounds_tablet.Intersects(overview_item2_bounds_tablet));
  EXPECT_FALSE(
      overview_item1_bounds_tablet.Contains(overview_item2_bounds_tablet));
  EXPECT_FALSE(
      overview_item2_bounds_tablet.Contains(overview_item1_bounds_tablet));

  // After returning to clamshell mode, the two individual items should remain
  // distinctly separate within the Overview grid, with no intersection of their
  // bounds.
  ExitTabletMode();
  EXPECT_EQ(2u, overview_grid->item_list().size());

  overview_item1 = GetOverviewItemForWindow(w1.get());
  overview_item2 = GetOverviewItemForWindow(w2.get());
  EXPECT_NE(overview_item1, overview_item2);
  const gfx::RectF overview_item1_bounds_clamshell =
      overview_item1->target_bounds();
  const gfx::RectF overview_item2_bounds_clamshell =
      overview_item2->target_bounds();
  EXPECT_FALSE(overview_item1_bounds_clamshell.Intersects(
      overview_item2_bounds_clamshell));
  EXPECT_FALSE(overview_item1_bounds_clamshell.Contains(
      overview_item2_bounds_clamshell));
  EXPECT_FALSE(overview_item2_bounds_clamshell.Contains(
      overview_item1_bounds_clamshell));
}

// Tests that transitioning to tablet mode while an `OverviewGroupItem` is in
// partial Overview mode does not result in a crash. Regression test for crash
// stack trace reported in http://b/346617565#comment20.
TEST_F(SnapGroupTabletConversionTest, TransitionToTabletInPartialOverview) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Create `w3` to partially occlude primary snapped `w1`.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 200, 200, 200)));
  std::unique_ptr<aura::Window> w4(CreateAppWindow());

  // Snap `w4` to secondary to start the partial Overview.
  //                                |
  //                                |-------+
  //     +----+    +------+------+  |       |
  //     | w3 |    |  w1  |  w2  |  |   w4  |
  //     |    |    |      |      |  |       |
  //     +----+    +------+------+  |       |
  //                                |-------+
  //                                |

  SnapOneTestWindow(w4.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio);
  VerifySplitViewOverviewSession(w4.get());

  auto* root_window = Shell::GetPrimaryRootWindow();
  const auto* overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Verify that the the `OverviewGroupItem` is removed and replaced with two
  // separate items after converting to tablet mode.
  SwitchToTabletMode();
  EXPECT_EQ(3u, overview_grid->item_list().size());
}

// -----------------------------------------------------------------------------
// SnapGroupMultipleSnapGroupsTest:

using SnapGroupMultipleSnapGroupsTest = SnapGroupTest;

// Tests the basic functionalities of multiple snap groups.
TEST_F(SnapGroupMultipleSnapGroupsTest, MultipleSnapGroups) {
  // Create the 1st snap group.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  auto* snap_group1 =
      snap_group_controller->GetSnapGroupForGivenWindow(w2.get());
  auto* snap_group_divider1 = snap_group1->snap_group_divider();

  // Create a new window (w3) and maximize it. This will temporarily clear any
  // visible snapped windows, allowing the second snap group to be initialized.
  std::unique_ptr<aura::Window> w3(CreateAppWindow(gfx::Rect(0, 0, 800, 600)));

  // Create a 2nd group using a different snap ratio from `group1`.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  std::unique_ptr<aura::Window> w5(CreateAppWindow());
  SnapOneTestWindow(w4.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  ClickOverviewItem(GetEventGenerator(), w5.get());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w4.get(), w5.get()));
  auto* snap_group2 =
      snap_group_controller->GetSnapGroupForGivenWindow(w4.get());
  auto* snap_group_divider2 = snap_group2->snap_group_divider();
  EXPECT_EQ(2u, snap_group_controller->snap_groups_for_testing().size());
  EXPECT_NE(snap_group_divider1, snap_group_divider2);
  aura::Window* divider1_window = snap_group_divider1->GetDividerWindow();
  aura::Window* divider2_window = snap_group_divider2->GetDividerWindow();

  // Spin the run loop to wait for the divider widgets to be closed and re-shown
  // during the 2nd snap group creation session. See
  // `SnapGroupController::OnOverviewModeStarting|EndingAnimationComplete()`.
  base::RunLoop().RunUntilIdle();

  // Ensure each snap group divider is directly attached to its associated
  // windows. Verify the stacking order is correct inside each group and across
  // different groups.
  auto* desk_container = desks_util::GetActiveDeskContainerForRoot(
      Shell::Get()->GetPrimaryRootWindow());
  EXPECT_THAT(desk_container->children(),
              ElementsAre(/*group_1*/ w1.get(), w2.get(), divider1_window,
                          /*maximized_window*/ w3.get(), /*group_2*/ w4.get(),
                          w5.get(), divider2_window));
}

// Tests that the snap group can be recalled with its original bounds with
// multiple snap groups.
TEST_F(SnapGroupMultipleSnapGroupsTest, MultipleSnapGroupsRecall) {
  UpdateDisplay("900x600");
  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  // Create the 1st snap group with 2/3 and 1/3 snap ratio.
  SnapOneTestWindow(w1.get(),
                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio);
  ASSERT_TRUE(IsInOverviewSession());
  ClickOverviewItem(GetEventGenerator(), w2.get());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  SnapGroup* snap_group1 =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  SplitViewDivider* snap_group_divider1 = snap_group1->snap_group_divider();
  const int divider_position1 = snap_group_divider1->divider_position();

  // Create a new window (w0) and maximize it. This will temporarily clear any
  // visible snapped windows, allowing the second snap group to be initialized.
  std::unique_ptr<aura::Window> w3(CreateAppWindow(gfx::Rect(0, 0, 800, 600)));

  // Create the 2nd group. Test the 2nd group's divider is at 1/2.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  std::unique_ptr<aura::Window> w5(CreateAppWindow());
  SnapTwoTestWindows(w4.get(), w5.get(), /*horizontal=*/true,
                     GetEventGenerator());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w4.get(), w5.get()));
  auto* snap_group2 =
      snap_group_controller->GetSnapGroupForGivenWindow(w4.get());
  auto* snap_group_divider2 = snap_group2->snap_group_divider();
  EXPECT_EQ(GetWorkAreaBounds().width() * chromeos::kDefaultSnapRatio -
                kSplitviewDividerShortSideLength / 2,
            snap_group_divider2->divider_position());

  // Activate `w2` to simulate selecting the window from the shelf. Test we
  // restore `w1`'s group.
  wm::ActivateWindow(w2.get());
  EXPECT_EQ(2u, snap_group_controller->snap_groups_for_testing().size());
  EXPECT_EQ(snap_group1, snap_group_controller->GetTopmostSnapGroup());

  // Spin the run loop to wait for the divider widgets to be closed and re-shown
  // during the 2nd snap group creation session. See
  // `SnapGroupController::OnOverviewModeStarting|EndingAnimationComplete()`.
  base::RunLoop().RunUntilIdle();

  // Verify the stacking order from bottom to top. The order for each group is:
  // {2nd_mru_window, mru_window, divider}, with `w0` on the bottom.
  auto* desk_container = desks_util::GetActiveDeskContainerForRoot(
      Shell::Get()->GetPrimaryRootWindow());
  aura::Window* divider1 = snap_group_divider1->GetDividerWindow();
  aura::Window* divider2 = snap_group_divider2->GetDividerWindow();
  EXPECT_THAT(desk_container->children(),
              ElementsAre(/*maximized_window*/ w3.get(), /*group_2*/ w4.get(),
                          w5.get(), divider2,
                          /*group_1*/ w1.get(), w2.get(), divider1));

  // Verify the bounds of the 1st group are restored.
  EXPECT_EQ(divider_position1, snap_group_divider1->divider_position());
  EXPECT_EQ(divider_position1, w1->GetBoundsInScreen().width());
  EXPECT_EQ(divider_position1 + kSplitviewDividerShortSideLength,
            w2->GetBoundsInScreen().x());
}

// In the faster split screen setup, selecting an individual window within an
// existing Snap Group forms a **new** Snap Group combining the already snapped
// window (from the partial overview) and the newly selected window.
TEST_F(SnapGroupMultipleSnapGroupsTest,
       SelectWindowInSnapGroupInFasterPartialOverview) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  // Create `w3` to partially occlude primary snapped `w1`.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 200, 200, 200)));
  std::unique_ptr<aura::Window> w4(CreateAppWindow());

  // Round #1: Test that selecting a snapped window doesn't change its snap
  // position (snap state) to fill the opposite snapped area case.
  // Snap `w4` to secondary to start the partial Overview and verify that the
  // snap group is visible;
  //                                |
  //                                |-------+
  //     +----+    +------+------+  |       |
  //     | w3 |    |  w1  |  w2  |  |   w4  |
  //     |    |    |      |      |  |       |
  //     +----+    +------+------+  |       |
  //                                |-------+
  //                                |

  SnapOneTestWindow(w4.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio);
  ASSERT_TRUE(IsInOverviewSession());
  VerifySplitViewOverviewSession(w4.get());

  auto* root_window = Shell::GetPrimaryRootWindow();
  const auto* overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Select `w1` in the Snap Group, which remains as primary snapped.
  ActivateWindowInOverviewGroupItem(w1.get(), event_generator,
                                    /*by_touch_gestures=*/false);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  ASSERT_FALSE(IsInOverviewSession());
  VerifyNotSplitViewOrOverviewSession(w1.get());

  SnapGroupController* snap_group_controller = SnapGroupController::Get();

  // A new Snap Group comprising windows `w1` and `w4` will be created.
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w4.get()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w4.get(),
                                   GetTopmostSnapGroupDivider());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Round #2: Test that selecting a snapped window that changes its snap
  // position (snap state) to fill the opposite snapped area case.
  wm::ActivateWindow(w3.get());
  // Set bounds on `w3` to partially occlude secondary snapped window `w2`.
  w3->SetBounds(gfx::Rect(500, 200, 100, 200));
  wm::ActivateWindow(w2.get());

  // Snap `w2` to primary to start the partial Overview and verify that the
  // snap group is visible;
  //
  //               |
  //               |
  //     +---------+
  //     |         | +----+    +------+------+
  //     |   w2    | | w3 |    |  w1  |  w4  |
  //     |         | |    |    |      |      |
  //     |         | +----+    +------+------+
  //     +---------+
  //               |
  //               |
  SnapOneTestWindow(w2.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  ASSERT_TRUE(IsInOverviewSession());
  VerifySplitViewOverviewSession(w2.get());

  overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w2->IsVisible());
  EXPECT_TRUE(w4->IsVisible());

  // Select `w1` in the Snap Group, which changes its snapped state from primary
  // snapped to secondary snapped.
  ActivateWindowInOverviewGroupItem(w1.get(), event_generator,
                                    /*by_touch_gestures=*/false);
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  ASSERT_FALSE(IsInOverviewSession());
  VerifyNotSplitViewOrOverviewSession(w2.get());

  // A new Snap Group comprising windows `w2` and `w1` will be created.
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w1.get()));
  UnionBoundsEqualToWorkAreaBounds(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
}

//  In the Overview, after manually setting up a partial overview by dragging to
//  snap a window, selecting a window from an existing Snap Group creates a
//  **new** Snap Group. This new group combines the selected window in the
//  previous Snap Group with the window already snapped in the partial overview.
TEST_F(SnapGroupMultipleSnapGroupsTest,
       SelectWindowInSnapGroupInManualPartialOverview) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 200, 200, 200)));

  // Start full Overview and verify that snap group will show.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  auto* root_window = Shell::GetPrimaryRootWindow();
  const auto* overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Round #1: Test that selecting a snapped window that doesn't change its snap
  // position (snap state) to fill the opposite snapped area case.
  // Drag to snap `w3` to primary position to start the partial Overview.
  //
  //               |
  //               |
  //     +---------+
  //     |         |    +------+------+
  //     |   w3    |    |  w1  |  w2  |
  //     |         |    |      |      |
  //     |         |    +------+------+
  //     +---------+
  //               |
  //               |

  DragItemToPoint(GetOverviewItemForWindow(w3.get()), gfx::Point(0, 300),
                  event_generator, /*by_touch_gestures=*/false, /*drop=*/true);
  ASSERT_TRUE(IsInOverviewSession());
  VerifySplitViewOverviewSession(w3.get());

  overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(1u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Select `w2` in the Snap Group, which remains as secondary snapped.
  ActivateWindowInOverviewGroupItem(w2.get(), event_generator,
                                    /*by_touch_gestures=*/false);
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  ASSERT_FALSE(IsInOverviewSession());
  VerifyNotSplitViewOrOverviewSession(w3.get());

  // A new Snap Group comprising windows `w3` and `w2` will be created.
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Round #2: Test that selecting a snapped window that changes its snap
  // position (snap state) to fill the opposite snapped area case.
  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Drag to snap `w1` to secondary position to start the partial Overview.
  //                        |
  //                        |---------+
  //     +------+------+    |         |
  //     |  w3  |  w2  |    |   w1    |
  //     |      |      |    |         |
  //     +------+------+    |         |
  //                        |---------+
  //                        |

  DragItemToPoint(GetOverviewItemForWindow(w1.get()), gfx::Point(800, 300),
                  event_generator, /*by_touch_gestures=*/false, /*drop=*/true);
  ASSERT_TRUE(IsInOverviewSession());
  VerifySplitViewOverviewSession(w1.get());

  overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(1u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  // Select `w2` in the Snap Group, which changes its snapped state from
  // secondary snapped to primary snapped.
  ActivateWindowInOverviewGroupItem(w2.get(), event_generator,
                                    /*by_touch_gestures=*/false);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  ASSERT_FALSE(IsInOverviewSession());
  VerifyNotSplitViewOrOverviewSession(w1.get());

  // A new Snap Group comprising windows `w2` and `w1` will be created.
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w1.get()));
  UnionBoundsEqualToWorkAreaBounds(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
}

TEST_F(SnapGroupMultipleSnapGroupsTest,
       NoCrashWhenLongTappingOnGroupItemInPartialOverview) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  // Create `w3` to partially occlude primary snapped `w1`.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(200, 200, 200, 200)));
  std::unique_ptr<aura::Window> w4(CreateAppWindow());

  //                                |
  //                                |-------+
  //     +----+    +------+------+  |       |
  //     | w3 |    |  w1  |  w2  |  |   w4  |
  //     |    |    |      |      |  |       |
  //     +----+    +------+------+  |       |
  //                                |-------+
  //                                |

  SnapOneTestWindow(w4.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio);
  OverviewSession* overview_session =
      OverviewController::Get()->overview_session();
  CHECK(overview_session);
  VerifySplitViewOverviewSession(w4.get());

  auto* root_window = Shell::GetPrimaryRootWindow();
  const auto* overview_grid = GetOverviewGridForRoot(root_window);
  ASSERT_TRUE(overview_grid);
  EXPECT_EQ(2u, overview_grid->item_list().size());
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());

  OverviewItemBase* group_item = GetOverviewItemForWindow(w1.get());
  gfx::Point location =
      gfx::ToRoundedPoint(group_item->target_bounds().CenterPoint());
  // Offset the location from the center a bit since the event is not be handled
  // in the center gap of the `OverviewGroupItem` yet.
  location.Offset(/*delta_x=*/10, /*delta_y=*/0);
  event_generator->set_current_screen_location(location);

  overview_session->InitiateDrag(group_item, gfx::PointF(location),
                                 /*is_touch_dragging=*/true, group_item);
  LongTapAt(event_generator, location);

  base::RunLoop().RunUntilIdle();
}

// -----------------------------------------------------------------------------
// SnapGroupSnapToReplaceTest:

using SnapGroupSnapToReplaceTest = SnapGroupTest;

// Tests that when dragging a window to 'snap replace' a visible window in a
// snap group, the original window is replaced and a new snap group is created.
TEST_F(SnapGroupSnapToReplaceTest, SnapToReplaceBasic) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_FALSE(GetSplitViewController()->InSplitViewMode());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Verify that split view remains inactive to avoid split view specific
  // behaviors such as auto-snap or showing cannot snap toast.
  EXPECT_FALSE(GetSplitViewController()->InSplitViewMode());
}

// Tests if the window being snapped to replace has a min size which is smaller
// than size calculated from the target snap ratio, it will be snapped at its
// min size instead.
TEST_F(SnapGroupSnapToReplaceTest, WindowWithMinimumSize) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Create a new `w3` with min size > 0.5f.
  const gfx::Size min_size(GetWorkAreaBounds().width() * 0.6f, 0);
  std::unique_ptr<aura::Window> w3 = CreateAppWindowWithMinSize(min_size);

  // Snap to replace `w3` on top of the group.
  event_generator->set_current_screen_location(GetDragPoint(w3.get()));
  event_generator->DragMouseTo(0, 100);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w3.get())->GetStateType());

  // Test `w3` snaps at its min size and the group bounds are updated.
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(min_size.width(), w3->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Test that if both the window being snapped to replace and the opposite window
// in the group to replace have min sizes that can't fit, we don't allow snap to
// replace.
TEST_F(SnapGroupSnapToReplaceTest, BothWindowsMinimumSizes) {
  // Create a snap group where `w2` has min size 0.45f.
  const int work_area_length = GetWorkAreaBounds().width();
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(
      CreateAppWindowWithMinSize(gfx::Size(work_area_length * 0.45f, 0)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Create `w3` with min size 0.6f.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindowWithMinSize(gfx::Size(work_area_length * 0.6f, 0)));

  // Snap to replace `w3` over `w1`. Since `w3` and `w2` would not fit, test we
  // don't replace `w1` in the group.
  event_generator->set_current_screen_location(GetDragPoint(w3.get()));
  event_generator->DragMouseTo(0, 100);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w3.get())->GetStateType());
  EXPECT_TRUE(w2->GetBoundsInScreen().Intersects(w3->GetBoundsInScreen()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w2.get(), w3.get()));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that if a third window is snapped via any method except the window
// layout menu, it also checks the snap ratio threshold, the previous snap
// group's layout will be preserved.
TEST_F(SnapGroupSnapToReplaceTest,
       SnapToReplaceWithNonWindowLayoutSnapActionSource) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const gfx::Rect w1_bounds(w1->GetBoundsInScreen());
  const gfx::Rect w2_bounds(w2->GetBoundsInScreen());

  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  const float w3_snap_ratio = 0.15f;
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped, w3_snap_ratio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);

  // Since the gap between `w3` and the opposite snapped `w2` exceeds the
  // threshold, we do not snap to replace.
  ASSERT_GT(std::abs(1.f - *WindowState::Get(w3.get())->snap_ratio() -
                     *WindowState::Get(w2.get())->snap_ratio()),
            kSnapToReplaceRatioDiffThreshold);
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(w1_bounds, w1->GetBoundsInScreen());
  EXPECT_EQ(w2_bounds, w2->GetBoundsInScreen());
}

// Test that the snap ratio difference is calculated before snap-to-replace when
// snapping from window layout menu. If it's below the threshold, the
// snap-to-replace action will occur. If not, we'll directly snap on top.
TEST_F(SnapGroupSnapToReplaceTest, SnapToReplaceWithRatioMargin) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  const float w1_snap_ratio = *WindowState::Get(w1.get())->snap_ratio();
  EXPECT_EQ(w1_snap_ratio, chromeos::kDefaultSnapRatio);
  EXPECT_EQ(*WindowState::Get(w1.get())->snap_ratio(),
            chromeos::kDefaultSnapRatio);

  // Snap the new window `w3` with `chromeos::kDefaultSnapRatio` snap ratio from
  // window layout menu. Since the difference between
  // `chromeos::kDefaultSnapRatio` and `w1_snap_ratio` is less than
  // `kSnapToReplaceRatioDiffThreshold`, replace w1 with w3. Maintain the
  // previous snap ratio in the snap group formed by `w1` and `w2`.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_LT(std::abs(w1_snap_ratio - chromeos::kDefaultSnapRatio),
            kSnapToReplaceRatioDiffThreshold);

  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(*WindowState::Get(w3.get())->snap_ratio(),
            chromeos::kDefaultSnapRatio);
  EXPECT_EQ(*WindowState::Get(w2.get())->snap_ratio(),
            chromeos::kDefaultSnapRatio);

  // Resize `w2` and `w3` so the snap ratio gap will exceed the threshold.
  ResizeDividerTo(event_generator, /*resize_point=*/gfx::Point(
                      GetWorkAreaBounds().width() * 0.8f,
                      GetWorkAreaBounds().CenterPoint().y()));

  // Snap the new window `w4` with `chromeos::kOneThirdSnapRatio` ratio. Since
  // the snap ratio gap between `w4` and the opposite snapped `w2` is
  // greater than `kSnapToReplaceRatioDiffThreshold`, we directly snap on top.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w4.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_GT(std::abs(1.f - *WindowState::Get(w2.get())->snap_ratio() -
                     *WindowState::Get(w4.get())->snap_ratio()),
            kSnapToReplaceRatioDiffThreshold);
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w4.get()));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w3.get()));
}

// Tests that when dragging another window to snap in Overview with the
// existence of snap group. The to-be-snapped window will not replace the window
// in the snap group. See http://b/333603509 for more details.
TEST_F(SnapGroupSnapToReplaceTest, DoNotSnapToReplaceSnapGroupInOverview) {
  std::unique_ptr<aura::Window> w0(
      CreateAppWindow(gfx::Rect(10, 10, 200, 100)));

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  ASSERT_FALSE(GetSplitViewController()->InSplitViewMode());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  auto* overview_item0 = GetOverviewItemForWindow(w0.get());
  event_generator->set_current_screen_location(
      gfx::ToRoundedPoint(overview_item0->target_bounds().CenterPoint()));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(gfx::Point(0, 0));
  event_generator->ReleaseLeftButton();
  EXPECT_EQ(WindowState::Get(w0.get())->GetStateType(),
            chromeos::WindowStateType::kPrimarySnapped);

  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w0.get(), w1.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w0.get(), w2.get()));
}

// Verify 'Search + Shift + G' replaces the window on the same side when a
// snapped window is stacked on a Snap Group.
TEST_F(SnapGroupSnapToReplaceTest, UseShortcutToGroupPerformSnapToReplace) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  // Create a snapped `w3` stacked above the Snap Group.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);

  // Press 'Search + Shift + G' to perform snap-to-replace i.e. replacing `w1`
  // in the Snap Group with `w3`.
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);

  SnapGroupController* snap_group_controller =
      Shell::Get()->snap_group_controller();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w4.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);

  // Press 'Search + Shift + G' to perform snap-to-replace again i.e. replacing
  // `w2` in the Snap Group with `w4`.
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w4.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w4.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that we can perform snap-to-replace with a floated window.
TEST_F(SnapGroupSnapToReplaceTest, SnapToReplaceWithFloatedWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_FALSE(GetSplitViewController()->InSplitViewMode());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  std::unique_ptr<aura::Window> floated_window(CreateAppWindow());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(floated_window.get())->IsFloated());

  SnapOneTestWindow(floated_window.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  ASSERT_FALSE(WindowState::Get(floated_window.get())->IsFloated());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(floated_window.get(),
                                                           w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Verify snap-to-replace is disallowed when attempting to snap an always-on-top
// window. See http://b/347356195 for more details.
TEST_F(SnapGroupSnapToReplaceTest, DisallowSnapToReplaceWithAlwaysOnTopWindow) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  ASSERT_FALSE(GetSplitViewController()->InSplitViewMode());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  std::unique_ptr<aura::Window> always_on_top_window(CreateAlwaysOnTopWindow());
  EXPECT_NE(w1->parent(), always_on_top_window->parent());
  SnapOneTestWindow(always_on_top_window.get(),
                    WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio);

  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(
      w1.get(), always_on_top_window.get()));
  EXPECT_FALSE(snap_group_controller->AreWindowsInSnapGroup(
      w2.get(), always_on_top_window.get()));
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// -----------------------------------------------------------------------------
// SnapGroupAutoSnapGroupTest:

using SnapGroupAutoSnapGroupTest = SnapGroupTest;

// Tests to verify that resnapping a window within a Snap Group to the same
// position but with a different snap ratio will update the existing group. See
// http://b/342230763 for more details.
TEST_F(SnapGroupAutoSnapGroupTest, ReSnapWindowWithDifferentSnapRatio) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  ASSERT_TRUE(GetTopmostSnapGroupDivider());

  // Re-snap `w2` to 1/3. Test we re-form the group with the divider at 2/3.
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const int work_area_width(GetWorkAreaBounds().width());
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that drag to snap will respect the opposite snapped window's snap
// ratio.
TEST_F(SnapGroupAutoSnapGroupTest, DragToSnap) {
  // Create a snap group at 2/3 and 1/3.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, w2.get());
  auto* snap_group_controller = Shell::Get()->snap_group_controller();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const int work_area_width(GetWorkAreaBounds().width());
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Drag out to unsnap `w1`, then re-snap. Test we re-form a group with the
  // divider at 2/3 to keep the opposite snapped `w2` at 1/3.
  event_generator->MoveMouseTo(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().left_center());
  ASSERT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Drag out to unsnap `w2`, then re-snap. Test we re-form a group with the
  // divider at 2/3 to keep the opposite snapped `w2` at 1/3.
  event_generator->MoveMouseTo(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().right_center());
  ASSERT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that drag out to unsnap, then drag back to snap without releasing the
// mouse will keep the group snap ratio. See bug in http://b/346624805.
TEST_F(SnapGroupAutoSnapGroupTest, DragToSnapWithoutReleasingMouse) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = Shell::Get()->snap_group_controller();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const int work_area_width(GetWorkAreaBounds().width());
  EXPECT_EQ(std::round(work_area_width * chromeos::kDefaultSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Drag out to unsnap `w1` without releasing the left button.
  event_generator->MoveMouseTo(GetDragPoint(w1.get()));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(GetWorkAreaBounds().CenterPoint());
  // At this point `w1` will look visually unsnapped but still have snapped
  // state.
  ASSERT_EQ(gfx::Rect(250, 252, 300, 300), w1->GetTargetBounds());
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  // Now snap `w1` back to primary then release. Test it snaps at 1/2 and
  // re-forms a group.
  event_generator->MoveMouseTo(GetWorkAreaBounds().left_center());
  event_generator->ReleaseLeftButton();
  ASSERT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(GetWorkAreaBounds().CenterPoint(),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that resizing the snap group, then dragging to re-snap works correctly.
TEST_F(SnapGroupAutoSnapGroupTest, ResizeThenDragToSnap) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = Shell::Get()->snap_group_controller();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Resize to arbitrary locations, then drag to re-snap.
  for (const int resize_delta : {-30, 0, 15}) {
    SCOPED_TRACE(base::StringPrintf("Resize delta: %d", resize_delta));
    const gfx::Point divider_center(
        GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
    event_generator->MoveMouseTo(divider_center);
    event_generator->PressLeftButton();
    const gfx::Point resize_point(divider_center +
                                  gfx::Vector2d(resize_delta, 0));
    event_generator->MoveMouseTo(resize_point, /*count=*/2);
    EXPECT_EQ(resize_point,
              GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());
    event_generator->ReleaseLeftButton();

    // Drag out to unsnap, then re-snap. Test it snaps at approximately the same
    // ratio.
    event_generator->MoveMouseTo(GetDragPoint(w1.get()));
    event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
    ASSERT_FALSE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    event_generator->DragMouseTo(GetWorkAreaBounds().left_center());
    ASSERT_TRUE(
        snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
    EXPECT_NEAR(resize_point.x(),
                GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x(),
                1);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());
  }
}

// Tests re-snapping to different ratios via the window layout menu.
TEST_F(SnapGroupAutoSnapGroupTest, WindowLayoutMenu) {
  // Create a snap group at 2/3 and 1/3.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  auto* event_generator = GetEventGenerator();
  ClickOverviewItem(event_generator, w2.get());
  auto* snap_group_controller = Shell::Get()->snap_group_controller();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  const int work_area_width(GetWorkAreaBounds().width());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Re-snap `w1` to 1/2.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(work_area_width * chromeos::kDefaultSnapRatio,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Re-snap `w1` to 2/3.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Re-snap `w2` to 1/2.
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(std::round(work_area_width * chromeos::kDefaultSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Re-snap `w2` to 1/3.
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(std::round(work_area_width * chromeos::kTwoThirdSnapRatio),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that even if we skip partial overview, a snap group is formed on window
// layout complete.
TEST_F(SnapGroupAutoSnapGroupTest, SkipPartialAndFormSnapGroup) {
  // Snap `w1` to 2/3, then skip partial overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  VerifySplitViewOverviewSession(w1.get());
  PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  VerifyNotSplitViewOrOverviewSession(w1.get());

  // Drag to snap `w2` to the opposite side. Test we form a group.
  wm::ActivateWindow(w2.get());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().right_center());
  EXPECT_EQ(WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  auto* snap_group_controller = Shell::Get()->snap_group_controller();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Drag out to unsnap `w1`, then re-snap with the shortcut.
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
  EXPECT_EQ(WindowStateType::kNormal,
            WindowState::Get(w1.get())->GetStateType());
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_ALT_DOWN);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that when the gap between the snapped window and opposite snapped
// window exceeds the threshold, we do not auto group.
TEST_F(SnapGroupAutoSnapGroupTest, SnapRatioGapThreshold) {
  UpdateDisplay("1000x800");

  // Snap `w1`, then resize it to be < 1/3.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  auto* event_generator = GetEventGenerator();
  const gfx::Point resize_point1(w1->GetBoundsInScreen().right_center());
  event_generator->MoveMouseTo(resize_point1);
  event_generator->DragMouseTo(250, resize_point1.y());
  ASSERT_LT(*WindowState::Get(w1.get())->snap_ratio(),
            chromeos::kOneThirdSnapRatio);

  // Now snap `w2` to 1/3 secondary.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  EXPECT_LT(*WindowState::Get(w1.get())->snap_ratio(),
            chromeos::kOneThirdSnapRatio);

  // Since the gap between `w1` and `w2` exceeds the threshold, we do not group.
  ASSERT_GT(GetSnapRatioGap(w1.get(), w2.get()),
            kSnapToReplaceRatioDiffThreshold);
  EXPECT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that when the overlap between the snapped window and opposite snapped
// window exceeds the threshold, we do not auto group.
TEST_F(SnapGroupAutoSnapGroupTest, SnapRatioOverlapThreshold) {
  UpdateDisplay("1000x800");

  // Snap `w1` to secondary, then resize it to be > 2/3.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  const gfx::Point resize_point(w1->GetBoundsInScreen().left_center());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(resize_point);
  event_generator->DragMouseTo(100, resize_point.y());
  ASSERT_GT(*WindowState::Get(w1.get())->snap_ratio(),
            chromeos::kTwoThirdSnapRatio);

  // Now snap `w2` to 2/3 primary.
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w2.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);

  // Since the overlap of `w1` and `w2` exceeds the threshold, we do not group.
  ASSERT_GT(GetSnapRatioGap(w1.get(), w2.get()),
            kSnapToReplaceRatioDiffThreshold);
  EXPECT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Verifies shelf rounded corners behavior in relation to Snap Group state.
// - By default (no Snap Group): Shelf corners are rounded.
// - Upon Snap Group creation (auto-group entry point): Corners become sharp.
// - Upon Snap Group break: Corners revert to default rounded state.
TEST_F(SnapGroupAutoSnapGroupTest, ShelfRoundedCornersInAutoGroupEntryPoint) {
  ShelfLayoutManager* shelf_layout_manager =
      AshTestBase::GetPrimaryShelf()->shelf_layout_manager();
  ASSERT_EQ(ShelfBackgroundType::kDefaultBg,
            shelf_layout_manager->shelf_background_type());

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);

  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kOneThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);

  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Test that Shelf will be updated to have sharp rounded corners.
  EXPECT_EQ(ShelfBackgroundType::kMaximized,
            shelf_layout_manager->shelf_background_type());

  // Drag `w1` out to break the group.
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().top_center());
  aura::test::TestWindowDelegate().set_window_component(HTCAPTION);
  event_generator->PressLeftButton();
  event_generator->MoveMouseBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w1.get())->is_dragged());
  event_generator->ReleaseLeftButton();
  ASSERT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Verify that Shelf restores its default background type with rounded
  // corners.
  EXPECT_EQ(ShelfBackgroundType::kDefaultBg,
            shelf_layout_manager->shelf_background_type());
}

// -----------------------------------------------------------------------------
// SnapGroupDisplayMetricsTest:

using SnapGroupDisplayMetricsTest = SnapGroupTest;

// Tests that snapped window and divider widget bounds scale dynamically with
// display changes, preserving their relative snap ratio.
TEST_F(SnapGroupDisplayMetricsTest, DisplayScaleChange) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  const float w1_snap_ratio = *WindowState::Get(w1.get())->snap_ratio();
  const float w2_snap_ratio = *WindowState::Get(w2.get())->snap_ratio();

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider->divider_widget());

  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto display_id = WindowTreeHostManager::GetPrimaryDisplayId();

  for (const bool zoom_in : {true, true, true, true, false, false, false}) {
    display_manager->ZoomDisplay(display_id, zoom_in);
    EXPECT_NEAR(w1_snap_ratio, *WindowState::Get(w1.get())->snap_ratio(), 0.01);
    EXPECT_NEAR(w2_snap_ratio, *WindowState::Get(w2.get())->snap_ratio(), 0.01);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), divider);
  }
}

// Tests that when rotating display, the bounds of the snapped windows and
// divider will be adjusted properly.
TEST_F(SnapGroupDisplayMetricsTest, DisplayRotation) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider->divider_widget());

  auto* display_manager = Shell::Get()->display_manager();
  for (auto rotation :
       {display::Display::ROTATE_270, display::Display::ROTATE_0}) {
    SCOPED_TRACE(
        base::StringPrintf("Screen rotation = %d", static_cast<int>(rotation)));
    display_manager->SetDisplayRotation(
        WindowTreeHostManager::GetPrimaryDisplayId(), rotation,
        display::Display::RotationSource::USER);
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), divider);
  }
}

// Tests no crash when scaling up the work area. Regression test for
// http://b/331991853.
TEST_F(SnapGroupDisplayMetricsTest, ScaleUpWorkArea) {
  UpdateDisplay("800x600");

  // Set the min width so that the windows fit in `zoom_factor_1` but not
  // `zoom_factor_2`.
  const gfx::Size min_size(370, 0);
  std::unique_ptr<aura::Window> w1(CreateAppWindowWithMinSize(min_size));
  std::unique_ptr<aura::Window> w2(CreateAppWindowWithMinSize(min_size));

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Zoom in once. Test we update the group bounds.
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  const int64_t primary_id =
      display::Screen::GetScreen()->GetPrimaryDisplay().id();
  const float zoom_factor_1 = 1.05f;
  ASSERT_EQ(zoom_factor_1,
            display_manager()->GetDisplayInfo(primary_id).zoom_factor());
  ASSERT_FALSE(w1->GetBoundsInScreen().Intersects(w2->GetBoundsInScreen()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Zoom in again. Since the windows no longer fit, test we break the group.
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  const float zoom_factor_2 = 1.1f;
  ASSERT_EQ(zoom_factor_2,
            display_manager()->GetDisplayInfo(primary_id).zoom_factor());
  ASSERT_TRUE(w1->GetBoundsInScreen().Intersects(w2->GetBoundsInScreen()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
}

// Tests that when scaling up work area in Overview to make snapped windows no
// longer fit within the work area, the Snap Group will be broken upon Overview
// exit. See http://b/339719019 and http://b/343803517 for more details.
TEST_F(SnapGroupDisplayMetricsTest, ScaleUpWorkAreaInOverview) {
  UpdateDisplay("800x600");

  // Set the min width so that the windows don't fit after zooming in using
  // keyboard shortcut.
  const gfx::Size min_size(395, 0);
  std::unique_ptr<aura::Window> w1(CreateAppWindowWithMinSize(min_size));
  std::unique_ptr<aura::Window> w2(CreateAppWindowWithMinSize(min_size));

  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  const auto* overview_grid =
      GetOverviewGridForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);

  // Zoom in to make the windows no longer fit, the Snap Group should be broken.
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN);
  const int64_t primary_id =
      display::Screen::GetScreen()->GetPrimaryDisplay().id();
  ASSERT_EQ(1.05f, display_manager()->GetDisplayInfo(primary_id).zoom_factor());
  ASSERT_TRUE(GetUnionScreenBoundsForWindow(w1.get()).Intersects(
      GetUnionScreenBoundsForWindow(w2.get())));

  ToggleOverview();
  ASSERT_FALSE(IsInOverviewSession());
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_FALSE(GetTopmostSnapGroupDivider());
}

// Tests that there is no crash when work area changed after snapping two
// windows. Docked mananifier is used as an example to trigger the work area
// change.
TEST_F(SnapGroupDisplayMetricsTest, DockedMagnifier) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* docked_mangnifier_controller =
      Shell::Get()->docked_magnifier_controller();
  docked_mangnifier_controller->SetEnabled(/*enabled=*/true);
}

// Tests verifying virtual keyboard activation/deactivation which triggers work
// area change works properly with Snap Group.
TEST_F(SnapGroupDisplayMetricsTest, VirtualKeyboard) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  SetVirtualKeyboardEnabled(/*enabled=*/true);
  auto* keyboard_controller = keyboard::KeyboardUIController::Get();
  keyboard_controller->ShowKeyboard(true);
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  keyboard_controller->HideKeyboardByUser();
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests verifying ChromeVox activation/deactivation which triggers work area
// change works properly with Snap Group.
TEST_F(SnapGroupDisplayMetricsTest, ChromeVox) {
  const gfx::Rect work_area_without_cvox(GetWorkAreaBounds());

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());

  // Enable ChromeVox panel.
  const int kAccessibilityPanelHeight = 45;
  std::unique_ptr<views::Widget> widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
                       nullptr, kShellWindowId_AccessibilityPanelContainer);
  SetAccessibilityPanelHeight(kAccessibilityPanelHeight);
  auto* a11y_controller = Shell::Get()->accessibility_controller();
  a11y_controller->spoken_feedback().SetEnabled(true);
  const gfx::Rect work_area_with_cvox(GetWorkAreaBounds());
  ASSERT_NE(work_area_without_cvox, work_area_with_cvox);
  EXPECT_TRUE(a11y_controller->spoken_feedback().enabled());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Disable ChromeVox panel.
  SetAccessibilityPanelHeight(0);
  a11y_controller->spoken_feedback().SetEnabled(false);
  ASSERT_EQ(work_area_without_cvox, GetWorkAreaBounds());
  EXPECT_FALSE(a11y_controller->spoken_feedback().enabled());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// -----------------------------------------------------------------------------
// SnapGroupMultiDisplayTest:

using SnapGroupMultiDisplayTest = SnapGroupTest;

// Tests that snapping two windows on an external display now works as expected,
// with both windows and the divider fully visible on the external display. This
// addresses the previous issue where the snapped window would be off-screen.
TEST_F(SnapGroupMultiDisplayTest, SnapGroupCreationOnExternalDisplay) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  // Create Snap Group on display #2.
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(900, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(1000, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);

  // Verify that both windows and divider are visible on display #2.
  VerifySnapGroupOnDisplay(snap_group, displays[1].id());

  // Start resizing to the left.
  auto* snap_group_divider = snap_group->snap_group_divider();
  const gfx::Point divider_point(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  event_generator->set_current_screen_location(divider_point);
  event_generator->PressLeftButton();

  // Resize to a point between `w1` and `w2`'s minimum sizes.
  const gfx::Point resize_point1 = gfx::Point(950, divider_point.y());
  const bool horizontal = IsLayoutHorizontal(displays[1]);
  // The windows have a default min width of 104.
  const int min_length = GetMinimumWindowLength(w1.get(), horizontal);
  ASSERT_EQ(min_length, GetMinimumWindowLength(w2.get(), horizontal));
  ASSERT_EQ(104, min_length);
  event_generator->MoveMouseTo(resize_point1, /*count=*/2);
  EXPECT_EQ(resize_point1.x(),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Resize to the left to a point less than `w1`'s minimum width.
  const gfx::Point resize_point2 = gfx::Point(810, divider_point.y());
  event_generator->MoveMouseTo(resize_point2, /*count=*/2);
  EXPECT_EQ(min_length, w1->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Resize to the right to a point less than `w2`'s minimum width.
  const gfx::Point resize_point3 = gfx::Point(1500, divider_point.y());
  event_generator->MoveMouseTo(resize_point3, /*count=*/2);
  EXPECT_EQ(min_length, w2->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
}

TEST_F(SnapGroupMultiDisplayTest, NoGapAfterSnapGroupCreation) {
  ui::ScopedAnimationDurationScaleMode animation_scale(
      ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);

  UpdateDisplay("1366x768,1367+0-1366x768");
  const gfx::Size window_minimum_size = gfx::Size(500, 0);

  for (const int window_x_origin : {0, 1367}) {
    SCOPED_TRACE(base::StringPrintf("window origin = %d", window_x_origin));

    aura::test::TestWindowDelegate delegate1;
    std::unique_ptr<aura::Window> w1(CreateTestWindowInShellWithDelegate(
        &delegate1, /*id=*/-1, gfx::Rect(window_x_origin, 0, 800, 600)));
    w1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
    delegate1.set_minimum_size(window_minimum_size);
    aura::test::TestWindowDelegate delegate2;
    std::unique_ptr<aura::Window> w2(CreateTestWindowInShellWithDelegate(
        &delegate2, /*id=*/-1, gfx::Rect(window_x_origin + 500, 0, 800, 600)));
    w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
    delegate2.set_minimum_size(window_minimum_size);

    SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                      chromeos::kTwoThirdSnapRatio);
    WaitForOverviewEntered();

    ClickOverviewItem(GetEventGenerator(), w2.get());
    EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
              WindowState::Get(w2.get())->GetStateType());
    WaitForOverviewExitAnimation();

    EXPECT_TRUE(GetTopmostSnapGroupDivider());
    UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                     GetTopmostSnapGroupDivider());
  }
}

// Tests that removing a display during split view overview session doesn't
// crash.
TEST_F(SnapGroupMultiDisplayTest, RemoveDisplay) {
  UpdateDisplay("800x600,801+0-800x600");
  display::test::DisplayManagerTestApi display_manager_test(display_manager());

  // Snap `window` on the second display to start split view overview session.
  std::unique_ptr<aura::Window> window1(
      CreateTestWindowInShellWithBounds(gfx::Rect(900, 0, 100, 100)));
  std::unique_ptr<aura::Window> window2(
      CreateTestWindowInShellWithBounds(gfx::Rect(1000, 0, 100, 100)));
  WindowState* window_state = WindowState::Get(window1.get());
  const WindowSnapWMEvent snap_type(
      WM_EVENT_SNAP_PRIMARY,
      /*snap_action_source=*/WindowSnapActionSource::kTest);
  window_state->OnWMEvent(&snap_type);
  ASSERT_EQ(display_manager_test.GetSecondaryDisplay().id(),
            display::Screen::GetScreen()
                ->GetDisplayNearestWindow(window1.get())
                .id());
  EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state->GetStateType());
  EXPECT_TRUE(OverviewController::Get()->InOverviewSession());
  EXPECT_TRUE(RootWindowController::ForWindow(window1.get())
                  ->split_view_overview_session());

  // Disconnect the second display. Test no crash.
  UpdateDisplay("800x600");
  base::RunLoop().RunUntilIdle();
}

// Tests to verify that when a window is dragged out of a snap group and onto
// another display, it snaps correctly with accurate bounds on the destination
// display. See regression at http://b/331663949.
TEST_F(SnapGroupMultiDisplayTest, DragWindowOutOfSnapGroupToAnotherDisplay) {
  UpdateDisplay("800x700,801+0-800x700,1602+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(3U, displays.size());

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  const gfx::Point point_in_display2(802, 0);
  ASSERT_FALSE(displays[0].bounds().Contains(point_in_display2));
  ASSERT_TRUE(displays[1].bounds().Contains(point_in_display2));

  event_generator->set_current_screen_location(GetDragPoint(w2.get()));
  event_generator->DragMouseTo(point_in_display2);

  ASSERT_FALSE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));

  display::Screen* screen = display::Screen::GetScreen();
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());
  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(w2.get())->GetStateType());

  gfx::Rect display1_left_half, display1_right_half;
  displays[1].work_area().SplitVertically(display1_left_half,
                                          display1_right_half);

  EXPECT_EQ(display1_left_half, w2->GetBoundsInScreen());
}

// Test that Search+Alt+M moves the snap group between displays.
TEST_F(SnapGroupMultiDisplayTest, MoveSnapGroupBetweenDisplays) {
  UpdateDisplay("800x600,1000x600");

  // Snap `w1` and `w2` on display 1.
  std::unique_ptr<aura::Window> w1(
      CreateTestWindowInShellWithBounds(gfx::Rect(0, 0, 100, 100)));
  w1->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  std::unique_ptr<aura::Window> w2(
      CreateTestWindowInShellWithBounds(gfx::Rect(0, 0, 100, 100)));
  w2->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::CHROME_APP);
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_divider = SnapGroupController::Get()
                                 ->GetSnapGroupForGivenWindow(w1.get())
                                 ->snap_group_divider();
  const int64_t primary_id =
      display::Screen::GetScreen()->GetPrimaryDisplay().id();
  display::Screen* screen = display::Screen::GetScreen();
  ASSERT_EQ(primary_id, screen->GetDisplayNearestWindow(w1.get()).id());
  ASSERT_EQ(primary_id, screen->GetDisplayNearestWindow(w2.get()).id());

  // Activate `w1`, then press Search+Alt+M to move it to display 2.
  wm::ActivateWindow(w1.get());
  PressAndReleaseKey(ui::VKEY_M, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  const int64_t secondary_id =
      display::test::DisplayManagerTestApi(display_manager())
          .GetSecondaryDisplay()
          .id();
  ASSERT_EQ(secondary_id, screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(secondary_id, screen->GetDisplayNearestWindow(w2.get()).id());
  aura::Window* divider_window = snap_group_divider->GetDividerWindow();
  EXPECT_EQ(secondary_id, screen->GetDisplayNearestWindow(divider_window).id());

  auto* desk_container = desks_util::GetActiveDeskContainerForRoot(
      Shell::Get()->GetRootWindowForDisplayId(secondary_id));

  MruWindowTracker* mru_window_tracker = Shell::Get()->mru_window_tracker();
  aura::Window* mru_window = window_util::GetTopMostWindow(
      mru_window_tracker->BuildMruWindowList(DesksMruType::kActiveDesk));

  // `w1` will be the mru window. With the window stacking fixed by
  // `window_util::FixWindowStackingAccordingToGlobalMru()`, the `w2` that gets
  // moved after will be stacked above `w1`.
  EXPECT_EQ(mru_window, w1.get());
  EXPECT_THAT(desk_container->children(),
              ElementsAre(w2.get(), w1.get(), divider_window));

  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
}

// Tests that moving an `OverviewGroupItem` between displays correctly
// relocates the group item and its windows without crashing, while maintaining
// divider widget invisibility during the overview session.
TEST_F(SnapGroupMultiDisplayTest, MoveSnapGroupBetweenDisplaysInOverview) {
  UpdateDisplay("800x700,801+0-800x700,1602+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(3U, displays.size());

  const gfx::Point point_in_display2(900, 100);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display2));
  EXPECT_TRUE(displays[1].bounds().Contains(point_in_display2));
  EXPECT_FALSE(displays[2].bounds().Contains(point_in_display2));

  const gfx::Point point_in_display3(1700, 200);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display3));
  EXPECT_FALSE(displays[1].bounds().Contains(point_in_display3));
  EXPECT_TRUE(displays[2].bounds().Contains(point_in_display3));

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* divider = GetTopmostSnapGroupDivider();
  ASSERT_TRUE(divider);
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  ASSERT_TRUE(divider_widget->IsVisible());

  struct {
    gfx::Point drop_location;
    int display_index;
  } kTestCases[]{
      {point_in_display2, 1}, {point_in_display3, 2}, {gfx::Point(0, 0), 0}};

  OverviewController* overview_controller = OverviewController::Get();
  for (const auto test_case : kTestCases) {
    SCOPED_TRACE("\nDrop location: " + test_case.drop_location.ToString() +
                 ";\n" + "Destination display index: " +
                 base::NumberToString(test_case.display_index) + ".");
    overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
    EXPECT_FALSE(divider_widget->IsVisible());

    auto* overview_group_item = GetOverviewItemForWindow(w1.get());
    DragGroupItemToPoint(overview_group_item, test_case.drop_location,
                         event_generator,
                         /*by_touch_gestures=*/false, /*drop=*/true);
    EXPECT_FALSE(divider_widget->IsVisible());

    display::Screen* screen = display::Screen::GetScreen();
    EXPECT_EQ(displays[test_case.display_index].id(),
              screen->GetDisplayNearestWindow(w1.get()).id());
    EXPECT_EQ(displays[test_case.display_index].id(),
              screen->GetDisplayNearestWindow(w2.get()).id());

    SendKeyUntilOverviewItemIsFocused(ui::VKEY_TAB, GetEventGenerator());
    event_generator->PressKey(ui::VKEY_RETURN, /*flags=*/0);

    EXPECT_TRUE(divider_widget->IsVisible());
  }
}

// Verifies that when an `OverviewGroupItem` is dragged between displays in
// Overview mode, both the item's widget and the windows are mirrored properly.
// See http://b/335463631 for more details.
TEST_F(SnapGroupMultiDisplayTest,
       MirrorSnapGroupWhenMovingAcrossDisplaysInOverview) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  const gfx::Point point_in_display2(1000, 100);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display2));
  EXPECT_TRUE(displays[1].bounds().Contains(point_in_display2));

  // Create Snap Group on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  // Move Snap Group to display #2.
  OverviewGroupItem* group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w1.get()));
  DragGroupItemToPoint(group_item, point_in_display2, event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/false);

  // Verify that the item widget and window are mirrored for the individual
  // items.
  for (const auto& item : group_item->overview_items_for_testing()) {
    EXPECT_TRUE(item->item_mirror_for_dragging_for_testing());
    EXPECT_TRUE(item->window_mirror_for_dragging_for_testing());
  }

  event_generator->ReleaseLeftButton();

  // Verify that the windows are moved to the destination display properly.
  display::Screen* screen = display::Screen::GetScreen();
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());
}

// Tests that when moving snap group to another display with snap group, the
// windows will be moved to the destination display properly.
TEST_F(SnapGroupMultiDisplayTest,
       MoveSnapGroupToAnotherDisplayWithSnapGroupInOverview) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  const gfx::Point point_in_display1(100, 10);
  EXPECT_TRUE(displays[0].bounds().Contains(point_in_display1));
  EXPECT_FALSE(displays[1].bounds().Contains(point_in_display1));

  const gfx::Point point_in_display2(1000, 100);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display2));
  EXPECT_TRUE(displays[1].bounds().Contains(point_in_display2));

  // Create Snap Group #1 on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());

  // Create Snap Group #2 on display #2.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(900, 0, 200, 100)));
  std::unique_ptr<aura::Window> w4(
      CreateAppWindow(gfx::Rect(1000, 50, 100, 200)));
  SnapTwoTestWindows(w3.get(), w4.get(), /*horizontal=*/true, event_generator);
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w3.get()),
      displays[1].id());

  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->StartOverview(OverviewStartAction::kOverviewButton);
  ASSERT_TRUE(overview_controller->InOverviewSession());

  // Move Snap Group #2 to display #1 and move Snap Group #1 to display #2.
  DragGroupItemToPoint(GetOverviewItemForWindow(w3.get()), point_in_display1,
                       event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), point_in_display2,
                       event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);

  // Verify that the windows are moved to the destination display properly.
  display::Screen* screen = display::Screen::GetScreen();
  EXPECT_EQ(displays[0].id(), screen->GetDisplayNearestWindow(w3.get()).id());
  EXPECT_EQ(displays[0].id(), screen->GetDisplayNearestWindow(w4.get()).id());
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());
}

// Tests that dragging an `OverviewGroupItem` to a different desk in another
// display will properly move the windows to the destination desk and display.
TEST_F(SnapGroupMultiDisplayTest,
       MoveSnapGroupToADifferentDeskInAnotherDisplayInOverview) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());
  aura::Window::Windows root_windows = Shell::GetAllRootWindows();
  ASSERT_EQ(2U, root_windows.size());
  auto* root2 = root_windows[1].get();

  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());

  // Create Snap Group on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());

  auto* overview_grid = GetOverviewGridForRoot(root2);
  ASSERT_TRUE(overview_grid);
  const auto* desks_bar_view = overview_grid->desks_bar_view();
  ASSERT_TRUE(desks_bar_view);
  const auto& desks_mini_views = desks_bar_view->mini_views();
  ASSERT_EQ(desks_mini_views.size(), 2u);

  // Drag `group_item` to the middle of the `desks_mini_views[1]` and drop.
  const auto drop_point =
      desks_mini_views[1]->GetBoundsInScreen().CenterPoint();
  DragGroupItemToPoint(GetOverviewItemForWindow(w1.get()), drop_point,
                       event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);

  // Activate `desks_mini_views[1]` to switch to a different desk.
  event_generator->MoveMouseTo(drop_point);
  DeskSwitchAnimationWaiter waiter;
  event_generator->ClickLeftButton();
  waiter.Wait();
  ASSERT_FALSE(IsInOverviewSession());

  display::Screen* screen = display::Screen::GetScreen();
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());
  EXPECT_TRUE(desks_util::IsActiveDeskContainer(w1->parent()));
  EXPECT_TRUE(desks_util::IsActiveDeskContainer(w2->parent()));
  EXPECT_TRUE(w1->IsVisible());
  EXPECT_TRUE(w2->IsVisible());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[1].id());
}

// Tests if a `SnapGroup` is created on the external display, desk change with
// will not move the `SnapGroup` to the internal display.
TEST_F(SnapGroupMultiDisplayTest, DeskChangeWithMultiDisplay) {
  UpdateDisplay("800x700,801+0-800x700");

  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  // Create Snap Group on display #2.
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(900, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(1000, 50, 100, 200)));
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[1].id());
  display::Screen* screen = display::Screen::GetScreen();
  ASSERT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  ASSERT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());

  auto* desks_controller = DesksController::Get();
  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
  ASSERT_EQ(2u, desks_controller->desks().size());
  const Desk* desk0 = desks_controller->GetDeskAtIndex(0);
  const Desk* desk1 = desks_controller->GetDeskAtIndex(1);
  ASSERT_TRUE(desk0->is_active());

  // Use `Search + ]` to switch to `desk1`.
  PressAndReleaseKey(ui::VKEY_OEM_6, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(desk1->is_active());

  // Use `Search + [` to switch back to `desk0`.
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_COMMAND_DOWN);
  DeskSwitchAnimationWaiter().Wait();
  ASSERT_TRUE(desk0->is_active());

  // The snap group remains on display #2 after desk switches.
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(displays[1].id(), screen->GetDisplayNearestWindow(w2.get()).id());
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[1].id());
}

// Tests that mirrored mode works correctly.
TEST_F(SnapGroupMultiDisplayTest, MirroredMode) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());
  const int64_t primary_id = displays[0].id();
  const int64_t secondary_id = displays[1].id();

  // Create Snap Group #1 on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  auto* group1 = snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  VerifySnapGroupOnDisplay(group1, primary_id);

  // Create Snap Group #2 on display #2.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(900, 0, 200, 100)));
  std::unique_ptr<aura::Window> w4(
      CreateAppWindow(gfx::Rect(1000, 50, 100, 200)));
  SnapTwoTestWindows(w3.get(), w4.get(), /*horizontal=*/true, event_generator);
  auto* group2 = snap_group_controller->GetSnapGroupForGivenWindow(w3.get());
  VerifySnapGroupOnDisplay(group2, secondary_id);

  // Enter mirrored mode.
  display_manager->SetMirrorMode(display::MirrorMode::kNormal, std::nullopt);
  ASSERT_EQ(1U, displays.size());
  VerifySnapGroupOnDisplay(group1, primary_id);
  VerifySnapGroupOnDisplay(group2, primary_id);

  // Exit mirrored mode.
  display_manager->SetMirrorMode(display::MirrorMode::kOff, std::nullopt);
  ASSERT_EQ(2U, displays.size());

  // TODO(b/325331792): Verify the group is restored to its display. Currently
  // we just verify the group bounds are visible and on-screen.
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   group1->snap_group_divider());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w4.get(),
                                   group2->snap_group_divider());
}

// Tests that toggling mirror mode with a Snap Group on external display doesn't
// result in crash. Regression test for http://b/358539486.
TEST_F(SnapGroupMultiDisplayTest, ToggleMirrorMode) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  const gfx::Point point_in_display2(1000, 100);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display2));
  EXPECT_TRUE(displays[1].bounds().Contains(point_in_display2));

  // Create Snap Group on display #2.
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(1000, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(1050, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  SnapGroup* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  VerifySnapGroupOnDisplay(snap_group, displays[1].id());

  // Enable mirror mode and there should be no crash.
  display_manager->SetMirrorMode(display::MirrorMode::kNormal, std::nullopt);
  ASSERT_EQ(1U, displays.size());
  VerifySnapGroupOnDisplay(snap_group, displays[0].id());
  base::RunLoop().RunUntilIdle();

  // Disable mirror mode and there should be no crash.
  display_manager->SetMirrorMode(display::MirrorMode::kOff, std::nullopt);
  ASSERT_EQ(2U, displays.size());
  VerifySnapGroupOnDisplay(snap_group, displays[1].id());
  base::RunLoop().RunUntilIdle();
}

// Tests drag to snap across a landscape and portrait display.
TEST_F(SnapGroupMultiDisplayTest, LandscapeAndPortrait) {
  UpdateDisplay("800x600,600x800");
  // Set `w1` on the bottom half of the display since we need to drag a vertical
  // movement > kSnapTriggerVerticalMoveThreshold in order to snap to top. See
  // `WorkspaceWindowResizer::Drag()`.
  std::unique_ptr<aura::Window> w1(
      CreateAppWindow(gfx::Rect(0, 400, 200, 200)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(800, 0, 200, 200)));

  // Drag to snap `w1` to primary on display 2.
  wm::ActivateWindow(w1.get());
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  const display::Display display2 =
      display::test::DisplayManagerTestApi(display_manager())
          .GetSecondaryDisplay();
  ASSERT_FALSE(IsLayoutHorizontal(display2));
  const gfx::Rect work_area2 = display2.work_area();
  event_generator->DragMouseTo(work_area2.top_center());
  gfx::Rect top_half, bottom_half;
  work_area2.SplitHorizontally(top_half, bottom_half);
  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(top_half, w1->GetBoundsInScreen());

  // Select `w2` to be auto-snapped.
  ClickOverviewItem(event_generator, w2.get());
  EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            WindowState::Get(w2.get())->GetStateType());
  EXPECT_TRUE(
      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Expect the divider is horizontal.
  const gfx::Rect divider_bounds(
      work_area2.x(),
      work_area2.CenterPoint().y() - kSplitviewDividerShortSideLength / 2,
      work_area2.width(), kSplitviewDividerShortSideLength);
  EXPECT_EQ(divider_bounds, GetTopmostSnapGroupDividerBoundsInScreen());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Tests that the snap group bounds are updated for removing and adding the
// primary display. Regression test for http://b/335313098.
TEST_F(SnapGroupMultiDisplayTest, AddRemovePrimaryDisplay) {
  UpdateDisplay("800x700,801+0-800x700");
  const int64_t primary_id = WindowTreeHostManager::GetPrimaryDisplayId();
  const int64_t secondary_id =
      display::test::DisplayManagerTestApi(display_manager())
          .GetSecondaryDisplay()
          .id();

  // Create Snap Group #1 on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  auto* group1 = snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  VerifySnapGroupOnDisplay(group1, primary_id);

  // Create Snap Group #2 on display #2.
  std::unique_ptr<aura::Window> w3(
      CreateAppWindow(gfx::Rect(801, 0, 200, 100)));
  std::unique_ptr<aura::Window> w4(
      CreateAppWindow(gfx::Rect(810, 50, 100, 200)));
  SnapTwoTestWindows(w3.get(), w4.get(), /*horizontal=*/true, event_generator);
  auto* group2 = snap_group_controller->GetSnapGroupForGivenWindow(w3.get());
  VerifySnapGroupOnDisplay(group2, secondary_id);

  // Disconnect primary display.
  display::ManagedDisplayInfo primary_info =
      display_manager()->GetDisplayInfo(primary_id);
  display::ManagedDisplayInfo secondary_info =
      display_manager()->GetDisplayInfo(secondary_id);
  std::vector<display::ManagedDisplayInfo> display_info_list;
  display_info_list.push_back(secondary_info);
  display_manager()->OnNativeDisplaysChanged(display_info_list);
  const auto& displays = display_manager()->active_display_list();
  ASSERT_EQ(1U, displays.size());
  ASSERT_EQ(WindowTreeHostManager::GetPrimaryDisplayId(), secondary_id);
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   group1->snap_group_divider());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w4.get(),
                                   group2->snap_group_divider());

  // Reconnect primary display.
  display_info_list.push_back(primary_info);
  display_manager()->OnNativeDisplaysChanged(display_info_list);
  ASSERT_EQ(2U, displays.size());
  ASSERT_EQ(WindowTreeHostManager::GetPrimaryDisplayId(), primary_id);
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   group1->snap_group_divider());
  UnionBoundsEqualToWorkAreaBounds(w3.get(), w4.get(),
                                   group2->snap_group_divider());
}

// Tests no overlap in the divider and window bounds after disconnecting and
// reconnecting the primary display.
TEST_F(SnapGroupMultiDisplayTest, AddRemovePrimaryDisplayAfterResize) {
  UpdateDisplay("1200x900,0+901-1200x900/u");
  ASSERT_EQ(2U, display_manager()->active_display_list().size());

  // Create Snap Group #1 on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  auto* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);

  // Resize via the divider to an arbitrary point.
  auto* snap_group_divider = snap_group->snap_group_divider();
  const gfx::Point divider_point(
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  event_generator->set_current_screen_location(divider_point);
  event_generator->PressLeftButton();
  const gfx::Point resize_point(350, divider_point.y());
  event_generator->MoveMouseTo(resize_point, /*count=*/22);
  event_generator->ReleaseLeftButton();
  EXPECT_EQ(resize_point.x(),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Disconnect the primary display.
  UpdateDisplay("1200x900/u");
  ASSERT_EQ(1U, display_manager()->active_display_list().size());
  UnionBoundsEqualToWorkAreaBounds(w2.get(), w1.get(), snap_group_divider);

  // Reconnect the primary display.
  UpdateDisplay("1200x900,0+901-1200x900/u");
  ASSERT_EQ(2U, display_manager()->active_display_list().size());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
}

// Tests that resizing via the cursor between displays works correctly.
TEST_F(SnapGroupMultiDisplayTest, ResizeCursorBetweenDisplays) {
  UpdateDisplay("800x700,801+0-800x700");
  const int min_width = 300;
  std::unique_ptr<aura::Window> w1(
      CreateAppWindowWithMinSize(gfx::Size(min_width, 0)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindowWithMinSize(gfx::Size(min_width, 0)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get());
  auto* snap_group_divider = snap_group->snap_group_divider();

  // Press and move the mouse right, within `w1` and `w2` min widths. Test we
  // update bounds.
  const gfx::Point divider_point(
      snap_group_divider->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint());
  event_generator->set_current_screen_location(divider_point);
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(gfx::Point(350, divider_point.y()), /*count=*/2);
  ASSERT_TRUE(snap_group_divider->is_resizing_with_divider());
  EXPECT_EQ(350, GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Move the mouse right, past `w2`'s min width and onto display #2. Test we
  // don't move beyond `w2`'s min width.
  event_generator->MoveMouseTo(gfx::Point(810, divider_point.y()), /*count=*/2);
  EXPECT_EQ(min_width, w2->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Move the mouse left, back onto display #1 but still beyond `w2`'s min
  // width. Test we don't move beyond `w2`'s min width.
  event_generator->MoveMouseTo(gfx::Point(799, divider_point.y()),
                               /*count=*/2);
  EXPECT_EQ(min_width, w2->GetBoundsInScreen().width());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);

  // Move the mouse left, back within range. Test we update bounds now.
  event_generator->MoveMouseTo(gfx::Point(350, divider_point.y()), /*count=*/2);
  EXPECT_EQ(350, GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint().x());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
  event_generator->ReleaseLeftButton();
}

// Verify that an Overview group item remains interactive after being dragged to
// a different display and back without releasing the mouse. Also verify the
// group item's widget consistently stays beneath the item widget of the
// individual windows it contains. See http://b/339088510 for more details.
TEST_F(SnapGroupMultiDisplayTest, GroupItemCrossDisplayDragInteractivity) {
  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());
  aura::Window::Windows root_windows = Shell::GetAllRootWindows();
  ASSERT_EQ(2U, root_windows.size());

  const gfx::Point point_in_display1(100, 10);
  EXPECT_TRUE(displays[0].bounds().Contains(point_in_display1));
  EXPECT_FALSE(displays[1].bounds().Contains(point_in_display1));

  const gfx::Point point_in_display2(1000, 100);
  EXPECT_FALSE(displays[0].bounds().Contains(point_in_display2));
  EXPECT_TRUE(displays[1].bounds().Contains(point_in_display2));

  // Create Snap Group on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w1.get()));
  ASSERT_TRUE(overview_group_item);

  const auto& overview_items =
      overview_group_item->overview_items_for_testing();
  ASSERT_EQ(2u, overview_items.size());
  auto* group_item_widget = overview_group_item->item_widget();
  ASSERT_TRUE(group_item_widget);
  auto* group_item_widget_window = group_item_widget->GetNativeWindow();

  // Stacking order verification before drag.
  for (const auto& overview_item : overview_items) {
    EXPECT_TRUE(window_util::IsStackedBelow(
        group_item_widget_window,
        overview_item->item_widget()->GetNativeWindow()));
  }

  // Drag `overview_group_item` to display #2 w/o releasing mouse and drag back
  // then drop.
  DragGroupItemToPoint(overview_group_item, point_in_display2, event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/false);
  DragGroupItemToPoint(overview_group_item, point_in_display1, event_generator,
                       /*by_touch_gestures=*/false, /*drop=*/true);

  // Stacking order verification after drag.
  for (const auto& overview_item : overview_items) {
    EXPECT_TRUE(window_util::IsStackedBelow(
        group_item_widget_window,
        overview_item->item_widget()->GetNativeWindow()));
  }

  // Verify that Overview exits on mouse click (shift the click position by
  // `gfx::Vector2d(10, 0)` as there is a gap between the two individual items
  // and it is not handling event currently), and both windows remaining on the
  // display they were originally on.
  event_generator->MoveMouseTo(
      gfx::ToRoundedPoint(overview_group_item->target_bounds().CenterPoint()) +
      gfx::Vector2d(10, 0));
  event_generator->ClickLeftButton();
  VerifyNotSplitViewOrOverviewSession(w1.get());

  display::Screen* screen = display::Screen::GetScreen();
  EXPECT_EQ(displays[0].id(), screen->GetDisplayNearestWindow(w1.get()).id());
  EXPECT_EQ(displays[0].id(), screen->GetDisplayNearestWindow(w2.get()).id());
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());
}

// Verify the following behavior when dragging an `OverviewGroupItem` to the new
// desk button on a different display:
// 1. The new desk button on the target display changes to
// `DeskIconButton::State::kActive`.
// 2. New desk buttons on other displays remain in
// `DeskIconButton::State::kExpanded`.
// 3. Upon dropping the OverviewItem, all new desk buttons (including the target
// display) are restored to `DeskIconButton::State::kExpanded` state.
TEST_F(SnapGroupMultiDisplayTest, NewDeskButtonStateUpdateOnMultiDisplay) {
  auto skip_scale_up_new_desk_button_duration = OverviewWindowDragController::
      SkipNewDeskButtonScaleUpDurationForTesting();

  UpdateDisplay("800x700,801+0-800x700");
  display::DisplayManager* display_manager = Shell::Get()->display_manager();
  const auto& displays = display_manager->active_display_list();
  ASSERT_EQ(2U, displays.size());

  const gfx::Point point_in_display1(502, 300);
  ASSERT_TRUE(displays[0].bounds().Contains(point_in_display1));
  ASSERT_FALSE(displays[1].bounds().Contains(point_in_display1));

  // Create Snap Group on display #1.
  std::unique_ptr<aura::Window> w1(CreateAppWindow(gfx::Rect(0, 0, 200, 100)));
  std::unique_ptr<aura::Window> w2(
      CreateAppWindow(gfx::Rect(50, 50, 100, 200)));
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  VerifySnapGroupOnDisplay(
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get()),
      displays[0].id());

  ToggleOverview();
  ASSERT_TRUE(IsInOverviewSession());
  ASSERT_TRUE(IsWindowInItsCorrespondingOverviewGrid(w1.get()));

  // Verify that the new desk buttons on both displays have
  // `DeskIconButton::State::kZero` state initially.
  const auto& grids = GetOverviewSession()->grid_list();
  ASSERT_EQ(2u, grids.size());
  auto* grid0 = grids[0].get();
  ASSERT_TRUE(grid0);
  auto* desks_bar_view0 = grid0->desks_bar_view();
  const DeskIconButton* new_desk_button0 = desks_bar_view0->new_desk_button();
  ASSERT_TRUE(new_desk_button0);
  ASSERT_TRUE(new_desk_button0->GetVisible());
  ASSERT_EQ(DeskIconButton::State::kZero, new_desk_button0->state());

  auto* grid1 = grids[1].get();
  ASSERT_TRUE(grid1);
  auto* desks_bar_view1 = grid1->desks_bar_view();
  const DeskIconButton* new_desk_button1 = desks_bar_view1->new_desk_button();
  ASSERT_TRUE(new_desk_button1);
  ASSERT_TRUE(new_desk_button1->GetVisible());
  ASSERT_EQ(DeskIconButton::State::kZero, new_desk_button1->state());

  OverviewItemBase* overview_group_item = GetOverviewItemForWindow(w1.get());
  ASSERT_TRUE(overview_group_item);

  // Drag the `overview_item` to new desk button on display #2 w/o releasing the
  // mouse. Verify that the new desk button on display #2 turns into
  // `DeskIconButton::State::kActive` state.
  DragGroupItemToPoint(
      overview_group_item, new_desk_button1->GetBoundsInScreen().CenterPoint(),
      event_generator, /*by_touch_gestures=*/false, /*drop=*/false);
  EXPECT_EQ(DeskIconButton::State::kExpanded, new_desk_button0->state());
  EXPECT_EQ(DeskIconButton::State::kActive, new_desk_button1->state());

  // Drag the `overview_group_item` back to display #1 w/o and drop. Verify that
  // the new desk buttons on all displays are restored to
  // `DeskIconButton::State::kExpanded` state.
  DragItemToPoint(overview_group_item, point_in_display1, event_generator,
                  /*by_touch_gestures=*/false, /*drop=*/true);
  EXPECT_EQ(DeskIconButton::State::kExpanded, new_desk_button0->state());
  EXPECT_EQ(DeskIconButton::State::kExpanded, new_desk_button1->state());
}

// -----------------------------------------------------------------------------
// SnapGroupA11yTest:

using SnapGroupA11yTest = SnapGroupTest;

// Tests that the divider receives system pane focus.
TEST_F(SnapGroupA11yTest, DividerPaneFocus) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);

  auto* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);
  auto* snap_group_divider = snap_group->snap_group_divider();

  // Test initially the divider is not active and focus ring is hidden.
  auto* divider_widget = snap_group_divider->divider_widget();
  EXPECT_FALSE(divider_widget->IsActive());

  auto* focus_ring =
      views::FocusRing::Get(snap_group_divider->divider_view_for_testing());
  ASSERT_TRUE(focus_ring);
  EXPECT_FALSE(focus_ring->GetVisible());

  // Cycle Backward. Test the divider is active and focus ring is shown.
  event_generator->PressKey(ui::VKEY_BROWSER_BACK, ui::EF_CONTROL_DOWN);
  EXPECT_TRUE(divider_widget->IsActive());
  EXPECT_TRUE(focus_ring->GetVisible());
  constexpr int kFocusRingPaddingDp = 8;
  EXPECT_TRUE(focus_ring->GetBoundsInScreen().ApproximatelyEqual(
      GetTopmostSnapGroupDividerBoundsInScreen(),
      /*tolerance=*/kFocusRingPaddingDp));

  // Cycle Backward. Test the divider loses active and focus ring is hidden.
  event_generator->PressKey(ui::VKEY_BROWSER_BACK, ui::EF_CONTROL_DOWN);
  EXPECT_FALSE(divider_widget->IsActive());
  EXPECT_FALSE(focus_ring->GetVisible());

  // Cycle Forward. Test the divider is active and focus ring is shown.
  event_generator->PressKey(ui::VKEY_BROWSER_FORWARD, ui::EF_CONTROL_DOWN);
  EXPECT_TRUE(divider_widget->IsActive());
  EXPECT_TRUE(focus_ring->GetVisible());
  EXPECT_TRUE(focus_ring->GetBoundsInScreen().ApproximatelyEqual(
      GetTopmostSnapGroupDividerBoundsInScreen(),
      /*tolerance=*/kFocusRingPaddingDp));
}

// Tests that the divider can be resized via the keyboard.
TEST_F(SnapGroupA11yTest, DividerResize) {
  TestAccessibilityControllerClient client;
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);
  auto* snap_group_divider = snap_group->snap_group_divider();

  auto* divider_widget = snap_group_divider->divider_widget();

  // Cycle focus to the divider.
  PressAndReleaseKey(ui::VKEY_BROWSER_BACK, ui::EF_CONTROL_DOWN);
  ASSERT_TRUE(divider_widget->IsActive());
  gfx::Point divider_center =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();

  // Resize left.
  PressAndReleaseKey(ui::VKEY_LEFT);
  EXPECT_EQ(divider_center + gfx::Vector2d(-kSplitViewDividerResizeDistance, 0),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_LEFT,
            client.last_a11y_alert());

  // Resize right.
  PressAndReleaseKey(ui::VKEY_RIGHT);
  EXPECT_EQ(divider_center,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_RIGHT,
            client.last_a11y_alert());

  // 2. Test with horizontal secondary display.
  UpdateDisplay("800x600/u");
  divider_center = GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();

  // Resize left.
  PressAndReleaseKey(ui::VKEY_LEFT);
  EXPECT_EQ(divider_center + gfx::Vector2d(-kSplitViewDividerResizeDistance, 0),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w2.get(), w1.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_LEFT,
            client.last_a11y_alert());

  // Resize right.
  PressAndReleaseKey(ui::VKEY_RIGHT);
  EXPECT_EQ(divider_center,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w2.get(), w1.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_RIGHT,
            client.last_a11y_alert());

  // 3. Test with vertical display.
  UpdateDisplay("600x800");
  divider_center = GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();

  // Resize up.
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(divider_center + gfx::Vector2d(0, -kSplitViewDividerResizeDistance),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_UP, client.last_a11y_alert());

  // Resize down.
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_EQ(divider_center,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_DOWN,
            client.last_a11y_alert());

  // 4. Test with vertical secondary display.
  UpdateDisplay("600x800/u");
  divider_center = GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();

  // Resize up.
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(divider_center + gfx::Vector2d(0, -kSplitViewDividerResizeDistance),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w2.get(), w1.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_UP, client.last_a11y_alert());

  // Resize down.
  PressAndReleaseKey(ui::VKEY_DOWN);
  EXPECT_EQ(divider_center,
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w2.get(), w1.get(), snap_group_divider);
  EXPECT_EQ(AccessibilityAlert::SNAP_GROUP_RESIZE_DOWN,
            client.last_a11y_alert());
}

// Tests that resize vertically works with ChromeVox on.
TEST_F(SnapGroupA11yTest, ResizeVertical) {
  UpdateDisplay("600x800");
  const gfx::Rect work_area_without_cvox(GetWorkAreaBounds());

  // Simulate enabling ChromeVox.
  const int kAccessibilityPanelHeight = 45;
  std::unique_ptr<views::Widget> widget =
      CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
                       nullptr, kShellWindowId_AccessibilityPanelContainer);
  SetAccessibilityPanelHeight(kAccessibilityPanelHeight);
  Shell::Get()->accessibility_controller()->spoken_feedback().SetEnabled(true);
  const gfx::Rect work_area_with_cvox(GetWorkAreaBounds());
  ASSERT_NE(work_area_without_cvox, work_area_with_cvox);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/false,
                     GetEventGenerator());
  auto* snap_group =
      SnapGroupController::Get()->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);
  auto* snap_group_divider = snap_group->snap_group_divider();
  const gfx::Point divider_center =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();

  // Note that the divider widget bounds are in screen, but `divider_position`
  // is relative to the work area.
  ASSERT_EQ(work_area_with_cvox.CenterPoint(), divider_center);
  ASSERT_EQ(
      GetTopmostSnapGroupDividerBoundsInScreen().y() - work_area_with_cvox.y(),
      snap_group_divider->divider_position());

  // Cycle focus to the divider.
  PressAndReleaseKey(ui::VKEY_BROWSER_BACK, ui::EF_CONTROL_DOWN);
  ASSERT_TRUE(snap_group_divider->divider_widget()->IsActive());

  // Resize up.
  ASSERT_EQ(GetWorkAreaBounds().CenterPoint(), divider_center);
  PressAndReleaseKey(ui::VKEY_UP);
  EXPECT_EQ(divider_center + gfx::Vector2d(0, -kSplitViewDividerResizeDistance),
            GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(), snap_group_divider);
}

// -----------------------------------------------------------------------------
// SnapGroupMetricsTest:

class SnapGroupMetricsTest : public SnapGroupTest {
 public:
  SnapGroupMetricsTest()
      : SnapGroupTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  ~SnapGroupMetricsTest() override = default;

  void AdvanceClock(base::TimeDelta delta) {
    task_environment()->AdvanceClock(delta);
    task_environment()->RunUntilIdle();
  }

 protected:
  base::HistogramTester histogram_tester_;
  base::UserActionTester user_action_tester_;
};

// Tests that the pipeline to get snap action source info all the way to be
// stored in the `SplitViewOverviewSession` is working. This test focuses on the
// snap action source with top-usage in clamshell.
TEST_F(SnapGroupMetricsTest, SnapActionSourcePipeline) {
  UpdateDisplay("800x600");
  std::unique_ptr<aura::Window> window1(CreateAppWindow(gfx::Rect(100, 100)));
  std::unique_ptr<aura::Window> window2(CreateAppWindow(gfx::Rect(200, 100)));

  // Drag a window to snap and verify the snap action source info.
  std::unique_ptr<WindowResizer> resizer(CreateWindowResizer(
      window1.get(), gfx::PointF(), HTCAPTION, wm::WINDOW_MOVE_SOURCE_MOUSE));
  resizer->Drag(gfx::PointF(0, 400), /*event_flags=*/0);
  resizer->CompleteDrag();
  resizer.reset();
  VerifySplitViewOverviewSession(window1.get());
  EXPECT_EQ(GetSplitViewOverviewSession(window1.get())
                ->snap_action_source_for_testing(),
            WindowSnapActionSource::kDragWindowToEdgeToSnap);
  MaximizeToClearTheSession(window1.get());

  // Mock snap from window layout menu and verify the snap action source info.
  chromeos::SnapController::Get()->CommitSnap(
      window1.get(), chromeos::SnapDirection::kSecondary,
      chromeos::kDefaultSnapRatio,
      chromeos::SnapController::SnapRequestSource::kWindowLayoutMenu);
  VerifySplitViewOverviewSession(window1.get());
  EXPECT_EQ(GetSplitViewOverviewSession(window1.get())
                ->snap_action_source_for_testing(),
            WindowSnapActionSource::kSnapByWindowLayoutMenu);
  MaximizeToClearTheSession(window1.get());

  // Mock snap from window snap button and verify the snap action source info.
  chromeos::SnapController::Get()->CommitSnap(
      window1.get(), chromeos::SnapDirection::kPrimary,
      chromeos::kDefaultSnapRatio,
      chromeos::SnapController::SnapRequestSource::kSnapButton);
  VerifySplitViewOverviewSession(window1.get());
  EXPECT_EQ(GetSplitViewOverviewSession(window1.get())
                ->snap_action_source_for_testing(),
            WindowSnapActionSource::kLongPressCaptionButtonToSnap);
  MaximizeToClearTheSession(window1.get());
}

// Verifies that the recorded duration of a snap group accurately reflects both
// its persistence and actual duration.
TEST_F(SnapGroupMetricsTest, SnapGroupDuration) {
  const std::string persistence_duration_histogram_name =
      BuildHistogramName(kSnapGroupPersistenceDurationRootWord);
  histogram_tester_.ExpectTotalCount(persistence_duration_histogram_name, 0);

  const std::string actual_duration_histogram_name =
      BuildHistogramName(kSnapGroupActualDurationRootWord);
  histogram_tester_.ExpectTotalCount(actual_duration_histogram_name, 0);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  SnapGroup* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  AdvanceClock(base::Seconds(10));

  // Snap `w3` to perform "snap to replace".
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w2.get()));
  EXPECT_FALSE(
      snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // `persistence_duration_histogram_name` remains as 0 as the lifespan of the
  // Snap Group persists.
  histogram_tester_.ExpectTotalCount(persistence_duration_histogram_name, 0);

  // Whereas for `actual_duration_histogram_name`, it recorded the actual Snap
  // Group duration.
  histogram_tester_.ExpectBucketCount(actual_duration_histogram_name,
                                      /*sample=*/10, /*expected_count=*/1);

  AdvanceClock(base::Seconds(10));

  snap_group_controller->RemoveSnapGroup(
      snap_group_controller->GetSnapGroupForGivenWindow(w2.get()),
      SnapGroupExitPoint::kDragWindowOut);
  histogram_tester_.ExpectBucketCount(persistence_duration_histogram_name,
                                      /*sample=*/20, /*expected_count=*/1);
  histogram_tester_.ExpectBucketCount(actual_duration_histogram_name,
                                      /*sample=*/10, /*expected_count=*/2);
}

// Verify `SnapGroupExitPoint` is correctly recorded across various Snap Group
// exit points (drag, window state change, destruction, tablet transition).
TEST_F(SnapGroupMetricsTest, SnapGroupExitPoint) {
  const std::string snap_group_exit_point =
      BuildHistogramName(kSnapGroupExitPointRootWord);
  histogram_tester_.ExpectTotalCount(snap_group_exit_point, 0);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SCOPED_TRACE("Test case 1: drag window out to exit");
  event_generator->MoveMouseTo(w1->GetBoundsInScreen().top_center());
  aura::test::TestWindowDelegate test_window_delegate;
  test_window_delegate.set_window_component(HTCAPTION);
  event_generator->PressLeftButton();
  event_generator->MoveMouseBy(50, 200);
  EXPECT_TRUE(WindowState::Get(w1.get())->is_dragged());
  event_generator->ReleaseLeftButton();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w2.get()));
  histogram_tester_.ExpectBucketCount(snap_group_exit_point,
                                      SnapGroupExitPoint::kDragWindowOut, 1);
  MaximizeToClearTheSession(w2.get());

  SCOPED_TRACE("Test case 2: maximize window to exit");
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapTwoTestWindows(w2.get(), w3.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w3.get()));
  WindowState* w2_state = WindowState::Get(w2.get());
  w2_state->Maximize();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w2.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w3.get()));
  histogram_tester_.ExpectBucketCount(
      snap_group_exit_point, SnapGroupExitPoint::kWindowStateChangedMaximized,
      1);
  MaximizeToClearTheSession(w2.get());

  SCOPED_TRACE("Test case 3: minimize window to exit");
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapTwoTestWindows(w3.get(), w4.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w3.get(), w4.get()));
  WindowState* w3_state = WindowState::Get(w3.get());
  w3_state->Minimize();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w3.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w4.get()));
  histogram_tester_.ExpectBucketCount(
      snap_group_exit_point, SnapGroupExitPoint::kWindowStateChangedMinimized,
      1);
  MaximizeToClearTheSession(w4.get());

  SCOPED_TRACE("Test case 4: float window to exit");
  std::unique_ptr<aura::Window> w5(CreateAppWindow());
  SnapTwoTestWindows(w4.get(), w5.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w4.get(), w5.get()));
  WindowState* w4_state = WindowState::Get(w4.get());
  const WindowFloatWMEvent float_event(
      chromeos::FloatStartLocation::kBottomRight);
  w4_state->OnWMEvent(&float_event);
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w4.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w5.get()));
  histogram_tester_.ExpectBucketCount(
      snap_group_exit_point, SnapGroupExitPoint::kWindowStateChangedFloated, 1);
  MaximizeToClearTheSession(w5.get());

  SCOPED_TRACE("Test case 5: window destruction to exit");
  std::unique_ptr<aura::Window> w6(CreateAppWindow());
  SnapTwoTestWindows(w5.get(), w6.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w5.get(), w6.get()));
  w5.reset();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w6.get()));
  histogram_tester_.ExpectBucketCount(
      snap_group_exit_point, SnapGroupExitPoint::kWindowDestruction, 1);

  SCOPED_TRACE("Test case 6: switch to tablet mode to exit");
  std::unique_ptr<aura::Window> w7(CreateAppWindow());
  SnapTwoTestWindows(w6.get(), w7.get(), /*horizontal=*/true, event_generator);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w6.get(), w7.get()));
  SwitchToTabletMode();
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w6.get()));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w7.get()));
  histogram_tester_.ExpectBucketCount(snap_group_exit_point,
                                      SnapGroupExitPoint::kTabletTransition, 1);
}

TEST_F(SnapGroupMetricsTest, SnapGroupsCount) {
  UpdateDisplay("800x600");

  const std::string snap_groups_count_histogram =
      BuildHistogramName(kSnapGroupsCountRootWord);

  histogram_tester_.ExpectTotalCount(snap_groups_count_histogram, 0);

  // Create and test we record 1 group.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_EQ(1u, snap_group_controller->snap_groups_for_testing().size());
  histogram_tester_.ExpectBucketCount(snap_groups_count_histogram,
                                      /*sample=*/1,
                                      /*expected_count=*/1);

  // Create a maximized window to occlude the snapped windows so we can start
  // partial overview and create a 2nd snap group.
  std::unique_ptr<aura::Window> w0(CreateAppWindow(gfx::Rect(0, 0, 800, 600)));

  // Create and test we record 2 groups.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapTwoTestWindows(w3.get(), w4.get(), /*horizontal=*/true, event_generator);
  ASSERT_EQ(2u, snap_group_controller->snap_groups_for_testing().size());
  histogram_tester_.ExpectBucketCount(snap_groups_count_histogram,
                                      /*sample=*/2,
                                      /*expected_count=*/1);

  // Close `w3` so we remove the 2nd snap group. Test we record 1 group.
  w3.reset();
  ASSERT_EQ(1u, snap_group_controller->snap_groups_for_testing().size());
  histogram_tester_.ExpectBucketCount(snap_groups_count_histogram,
                                      /*sample=*/1,
                                      /*expected_count=*/2);

  // At this point `w4` is active but a single snapped window. Recall the group
  // for `w1` and `w2` so we can start snap to replace.
  wm::ActivateWindow(w1.get());
  ASSERT_TRUE(
      snap_group_controller->GetTopmostVisibleSnapGroup(w1->GetRootWindow()));

  // Snap to replace `w5` in the 1st snap group. Test we don't record.
  std::unique_ptr<aura::Window> w5(CreateAppWindow());
  ASSERT_EQ(1u, snap_group_controller->snap_groups_for_testing().size());
  SnapOneTestWindow(w5.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_EQ(1u, snap_group_controller->snap_groups_for_testing().size());
  histogram_tester_.ExpectBucketCount(snap_groups_count_histogram,
                                      /*sample=*/1,
                                      /*expected_count=*/2);

  histogram_tester_.ExpectTotalCount(snap_groups_count_histogram, 3);
}

// Validate the accurate recording for 'Search + Shift + G' shortcut histogram.
TEST_F(SnapGroupMetricsTest, KeyboardshortcutToCreateSnapGroupHistogram) {
  const std::string histogram_name = "Ash.Accelerators.Actions.CreateSnapGroup";

  // Initially histogram is recorded as 0.
  histogram_tester_.ExpectTotalCount(histogram_name, 0);

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());

  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);
  SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);

  // Press 'Search + Shift + G' to to group `w1` and `w2`.
  auto* event_generator = GetEventGenerator();
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);

  SnapGroupController* snap_group_controller =
      Shell::Get()->snap_group_controller();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w2.get(),
                                   GetTopmostSnapGroupDivider());

  // Verify that the histogram is recorded correctly.
  histogram_tester_.ExpectTotalCount(histogram_name, 1);

  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kSecondarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kKeyboardShortcutToSnap);

  // Press 'Search + Shift + G' to to perform snap-to-replace.
  event_generator->PressAndReleaseKey(ui::VKEY_G,
                                      ui::EF_SHIFT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w3.get()));
  EXPECT_TRUE(GetTopmostSnapGroupDivider());
  UnionBoundsEqualToWorkAreaBounds(w1.get(), w3.get(),
                                   GetTopmostSnapGroupDivider());

  // Validate histogram counter increments.
  histogram_tester_.ExpectTotalCount(histogram_name, 2);
}

TEST_F(SnapGroupMetricsTest, SnapGroupUserActions) {
  UpdateDisplay("800x600");

  // Add a snap group, which will incidentally start partial overview.
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(
      user_action_tester_.GetActionCount("SnapGroups_StartPartialOverview"), 1);
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_AddSnapGroup"), 1);

  // Snap to replace.
  std::unique_ptr<aura::Window> w3(CreateAppWindow());
  SnapOneTestWindow(w3.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w2.get(), w3.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_SnapToReplace"), 1);

  // Resize `w3` to be < 1/3 so the snap ratio gap exceeds the threshold.
  const gfx::Point resize_point(w3->GetBoundsInScreen().right_center());
  event_generator->MoveMouseTo(resize_point);
  event_generator->DragMouseTo(150, resize_point.y());

  // Snap via the window layout menu with a ratio >
  // `kSnapToReplaceRatioDiffThreshold` to directly snap on top.
  std::unique_ptr<aura::Window> w4(CreateAppWindow());
  SnapOneTestWindow(w4.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kTwoThirdSnapRatio,
                    WindowSnapActionSource::kSnapByWindowLayoutMenu);
  ASSERT_GT(GetSnapRatioGap(w4.get(), w2.get()),
            kSnapToReplaceRatioDiffThreshold);
  ASSERT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w4.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_SnapDirect"), 1);

  // Remove the snap group.
  w3.reset();
  ASSERT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w3.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_RemoveSnapGroup"),
            1);
}

TEST_F(SnapGroupMetricsTest, RecallSnapGroupUserAction) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  ASSERT_TRUE(wm::IsActiveWindow(w2.get()));

  // Create a maximized window to occlude the snap group, then activate `w1` to
  // recall the group.
  std::unique_ptr<aura::Window> w3(CreateAppWindow(GetWorkAreaBounds()));
  wm::ActivateWindow(w1.get());
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_RecallSnapGroup"),
            1);

  // Enter overview, then click the overview group item to recall the group.
  ToggleOverview();
  ASSERT_TRUE(
      wm::IsActiveWindow(GetOverviewSession()->GetOverviewFocusWindow()));
  OverviewGroupItem* overview_group_item =
      static_cast<OverviewGroupItem*>(GetOverviewItemForWindow(w1.get()));
  ASSERT_TRUE(overview_group_item);
  GetOverviewSession()->SelectWindow(overview_group_item);
  ASSERT_TRUE(wm::IsActiveWindow(w1.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_RecallSnapGroup"),
            2);

  // Activate `w3`, then window cycle to `w1` to recall the group.
  wm::ActivateWindow(w3.get());
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/1);
  CompleteWindowCycling();
  ASSERT_TRUE(wm::IsActiveWindow(w1.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_RecallSnapGroup"),
            3);

  // Window cycle to `w2`. Test we don't record since the other `w1` was
  // previously active.
  CycleWindow(WindowCyclingDirection::kForward, /*steps=*/1);
  CompleteWindowCycling();
  ASSERT_TRUE(wm::IsActiveWindow(w2.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_RecallSnapGroup"),
            3);
}

TEST_F(SnapGroupMetricsTest, SkipFormSnapGroupAfterSnapping) {
  UpdateDisplay("800x600");

  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  std::unique_ptr<aura::Window> w1(CreateAppWindow());

  // Snap using the keyboard shortcut won't record.
  PressAndReleaseKey(ui::VKEY_OEM_4, ui::EF_ALT_DOWN);
  EXPECT_EQ(WindowStateType::kPrimarySnapped,
            WindowState::Get(w1.get())->GetStateType());
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_SkipFormSnapGroupAfterSnapping"),
            0);

  // Snap using an invalid snap action source won't record.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kSnapByWindowStateRestore);
  VerifyNotSplitViewOrOverviewSession(w1.get());
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_SkipFormSnapGroupAfterSnapping"),
            0);

  // Test that just skipping partial overview normally won't record.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
  VerifyNotSplitViewOrOverviewSession(w1.get());
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_SkipFormSnapGroupAfterSnapping"),
            0);

  // Selecting the 2nd window in partial overview won't record and will create a
  // snap group.
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio);
  VerifySplitViewOverviewSession(w1.get());
  ClickOverviewItem(GetEventGenerator(), w2.get());
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_SkipFormSnapGroupAfterSnapping"),
            0);
  auto* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_AddSnapGroup"), 1);

  // Drag out `w1` from the group, which will break the group, then re-snap it.
  // This will create a new snap group.
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(GetDragPoint(w1.get()));
  event_generator->DragMouseTo(GetWorkAreaBounds().CenterPoint());
  ASSERT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(w1.get()));
  SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
                    chromeos::kDefaultSnapRatio,
                    WindowSnapActionSource::kDragWindowToEdgeToSnap);
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
  EXPECT_EQ(user_action_tester_.GetActionCount("SnapGroups_AddSnapGroup"), 2);
}

// Verifies that the "double tap to swap windows" user action metrics are
// recorded accurately.
TEST_F(SnapGroupMetricsTest, DoubleTapDividerUserAction) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  auto* event_generator = GetEventGenerator();
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true, event_generator);
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  SplitViewDivider* divider = GetTopmostSnapGroupDivider();
  auto* divider_widget = divider->divider_widget();
  ASSERT_TRUE(divider_widget);
  auto* divider_view = divider->divider_view_for_testing();
  ASSERT_TRUE(divider_view);
  auto* handler_view = divider_view->handler_view_for_testing();
  ASSERT_TRUE(handler_view);

  const auto divider_center_point =
      GetTopmostSnapGroupDividerBoundsInScreen().CenterPoint();
  event_generator->set_current_screen_location(divider_center_point);
  event_generator->DoubleClickLeftButton();
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));

  // Verify that the correct user action metrics are recorded after a
  // successful window swap triggered by double-click on the divider.
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_DoubleTapWindowSwapAttempts"),
            1);
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_DoubleTapWindowSwapSuccess"),
            1);

  // Verify that after a successful window swap initiated by a double-tap on the
  // divider, the corresponding user action metrics are incremented.
  event_generator->GestureTapAt(divider_center_point);
  event_generator->GestureTapAt(divider_center_point);
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_DoubleTapWindowSwapAttempts"),
            2);
  EXPECT_EQ(user_action_tester_.GetActionCount(
                "SnapGroups_DoubleTapWindowSwapSuccess"),
            2);
}

TEST_F(SnapGroupMetricsTest, GroupContainerCycleViewAccessibleProperties) {
  std::unique_ptr<aura::Window> w1(CreateAppWindow());
  std::unique_ptr<aura::Window> w2(CreateAppWindow());
  SnapTwoTestWindows(w1.get(), w2.get(), /*horizontal=*/true,
                     GetEventGenerator());
  auto* snap_group_controller = SnapGroupController::Get();
  auto* snap_group =
      snap_group_controller->GetSnapGroupForGivenWindow(w1.get());
  ASSERT_TRUE(snap_group);
  std::unique_ptr<GroupContainerCycleView> cycle_view =
      std::make_unique<GroupContainerCycleView>(snap_group);
  ui::AXNodeData data;

  cycle_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.role, ax::mojom::Role::kGroup);
}

}  // namespace ash