chromium/ash/wm/float/float_controller_unittest.cc

// Copyright 2021 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/float/float_controller.h"

#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/accessibility/magnifier/docked_magnifier_controller.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/cursor_manager_chromeos.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desk_mini_view.h"
#include "ash/wm/desks/desks_test_api.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/float/float_test_api.h"
#include "ash/wm/float/tablet_mode_float_window_resizer.h"
#include "ash/wm/float/tablet_mode_tuck_education.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/scoped_window_tucker.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_metrics_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/test/test_non_client_frame_view_ash.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/work_area_insets.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/simple_test_clock.h"
#include "base/time/clock.h"
#include "chromeos/ui/base/app_types.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 "chromeos/ui/frame/header_view.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "chromeos/ui/wm/constants.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/window_observer.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/display_switches.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/test/test_widget_observer.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// Gets the header view for `window` so it can be dragged.
chromeos::HeaderView* GetHeaderView(aura::Window* window) {
  // Exiting immersive mode because of float does not seem to trigger a layout
  // like it does in production code. Here we force a layout, otherwise the
  // client view will remain the size of the widget, and dragging it will give
  // us HTCLIENT.
  auto* frame = NonClientFrameViewAsh::Get(window);
  DCHECK(frame);
  views::test::RunScheduledLayout(frame);
  return frame->GetHeaderView();
}

// Checks if `window` is being visibly animating. That means windows that are
// animated with tween zero are excluded because those jump to the target at the
// end of the animation.
bool IsVisiblyAnimating(aura::Window* window) {
  DCHECK(window);
  ui::LayerAnimator* animator = window->layer()->GetAnimator();
  return animator->is_animating() && animator->tween_type() != gfx::Tween::ZERO;
}

// Counts the amount of times a window's layer has be recreated. Used to ensure
// we are not using the cross-fade animation while dragging.
class WindowLayerRecreatedCounter : public aura::WindowObserver {
 public:
  explicit WindowLayerRecreatedCounter(aura::Window* window) {
    window_observation_.Observe(window);
  }
  WindowLayerRecreatedCounter(const WindowLayerRecreatedCounter&) = delete;
  WindowLayerRecreatedCounter& operator=(const WindowLayerRecreatedCounter&) =
      delete;
  ~WindowLayerRecreatedCounter() override = default;

  int recreated_count() const { return recreated_count_; }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    window_observation_.Reset();
  }
  void OnWindowLayerRecreated(aura::Window* window) override {
    ++recreated_count_;
  }

 private:
  int recreated_count_ = 0;

  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};
};

}  // namespace

class WindowFloatTest : public AshTestBase {
 public:
  WindowFloatTest() = default;
  explicit WindowFloatTest(base::test::TaskEnvironment::TimeSource time)
      : AshTestBase(time) {}
  WindowFloatTest(const WindowFloatTest&) = delete;
  WindowFloatTest& operator=(const WindowFloatTest&) = delete;
  ~WindowFloatTest() override = default;

  // Creates a floated application window.
  std::unique_ptr<aura::Window> CreateFloatedWindow() {
    std::unique_ptr<aura::Window> floated_window = CreateAppWindow();
    PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
    CHECK(WindowState::Get(floated_window.get())->IsFloated());
    return floated_window;
  }
};

// Test float/unfloat window.
TEST_F(WindowFloatTest, WindowFloatingSwitch) {
  // Activate `window_1` and perform floating.
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());

  // Activate `window_2` and perform floating.
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());

  // Only one floated window is allowed so when a different window is floated,
  // the previously floated window will be unfloated.
  EXPECT_FALSE(WindowState::Get(window_1.get())->IsFloated());

  // When try to float the already floated `window_2`, it will unfloat this
  // window.
  wm::ActivateWindow(window_2.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(WindowState::Get(window_2.get())->IsFloated());
}

// Tests that double clicking on the caption maximizes a floated window.
// Regression test for https://crbug.com/1357049.
TEST_F(WindowFloatTest, DoubleClickOnCaption) {
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Double click on the caption. The window should be maximized now.
  chromeos::HeaderView* header_view = GetHeaderView(window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(
      header_view->GetBoundsInScreen().CenterPoint());
  event_generator->DoubleClickLeftButton();

  EXPECT_TRUE(WindowState::Get(window.get())->IsMaximized());
}

// Tests that a floated window animates to and from overview.
TEST_F(WindowFloatTest, FloatWindowAnimatesInOverview) {
  std::unique_ptr<aura::Window> floated_window = CreateFloatedWindow();
  std::unique_ptr<aura::Window> maximized_window = CreateAppWindow();

  const WMEvent maximize_event(WM_EVENT_MAXIMIZE);
  WindowState::Get(maximized_window.get())->OnWMEvent(&maximize_event);

  // Activate `maximized_window`. If the other window was not floated, then it
  // would be hidden behind the maximized window and not animate.
  wm::ActivateWindow(maximized_window.get());

  // Enter overview, both windows should animate when entering overview, since
  // both are visible to the user.
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  ToggleOverview();
  EXPECT_TRUE(floated_window->layer()->GetAnimator()->is_animating());
  EXPECT_TRUE(maximized_window->layer()->GetAnimator()->is_animating());

  // Both windows should animate when exiting overview as well.
  WaitForOverviewEnterAnimation();
  ToggleOverview();
  EXPECT_TRUE(floated_window->layer()->GetAnimator()->is_animating());
  EXPECT_TRUE(maximized_window->layer()->GetAnimator()->is_animating());
}

// Tests that a floated window animates when a state change causes it to
// unfloat. Regression test for b/252505434.
TEST_F(WindowFloatTest, FloatToMaximizeWindowAnimates) {
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  const WMEvent maximize_event(WM_EVENT_MAXIMIZE);
  WindowState::Get(window.get())->OnWMEvent(&maximize_event);
  // `WindowState::SetBoundsDirectCrossFade` still starts an animation if the
  // source and destination bounds are the same. Therefore, it is not enough to
  // just check if its animating.
  EXPECT_TRUE(window->layer()->GetAnimator()->is_animating());
  EXPECT_NE(window->layer()->transform(),
            window->layer()->GetTargetTransform());
}

// Test when float a window in clamshell mode, window will change to default
// float bounds in certain conditions.
TEST_F(WindowFloatTest, WindowFloatingResize) {
  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(200, 200));

  // Float maximized window.
  auto* window_state = WindowState::Get(window.get());
  window_state->Maximize();
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(window_state->IsFloated());
  gfx::Rect default_float_bounds =
      FloatController::GetFloatWindowClamshellBounds(
          window.get(), chromeos::FloatStartLocation::kBottomRight);
  EXPECT_EQ(default_float_bounds, window->GetBoundsInScreen());
  // Unfloat.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(window_state->IsFloated());
  EXPECT_TRUE(window_state->IsMaximized());

  // Float full screen window.
  const WMEvent toggle_fullscreen_event(WM_EVENT_TOGGLE_FULLSCREEN);
  window_state->OnWMEvent(&toggle_fullscreen_event);
  EXPECT_TRUE(window_state->IsFullscreen());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(window_state->IsFloated());
  default_float_bounds = FloatController::GetFloatWindowClamshellBounds(
      window.get(), chromeos::FloatStartLocation::kBottomRight);
  EXPECT_EQ(default_float_bounds, window->GetBoundsInScreen());
  // Unfloat.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(window_state->IsFloated());
  // Unfloat a previously full screened window will restore to previous window
  // state.
  EXPECT_TRUE(window_state->IsMaximized());

  // Float window again.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  // Minimize floated window.
  // Minimized window can't be floated, but when a floated window enter/exit
  // minimized state, it remains floated.
  EXPECT_TRUE(window_state->IsFloated());
  const gfx::Rect curr_bounds = window->GetBoundsInScreen();
  window_state->Minimize();
  window_state->Restore();
  EXPECT_EQ(curr_bounds, window->GetBoundsInScreen());
  EXPECT_TRUE(window_state->IsFloated());

  // Float Snapped window.
  // Create a snap enabled window.
  auto window2 = CreateAppWindow(default_float_bounds);
  auto* window_state2 = WindowState::Get(window2.get());
  AcceleratorControllerImpl* acc_controller =
      Shell::Get()->accelerator_controller();

  // Snap Left.
  acc_controller->PerformActionIfEnabled(
      AcceleratorAction::kWindowCycleSnapLeft, {});
  ASSERT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            window_state2->GetStateType());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_EQ(default_float_bounds, window2->GetBoundsInScreen());

  // Unfloat.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  // Window back to snapped state.
  ASSERT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            window_state2->GetStateType());

  // Snap Right.
  acc_controller->PerformActionIfEnabled(
      AcceleratorAction::kWindowCycleSnapRight, {});
  ASSERT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            WindowState::Get(window2.get())->GetStateType());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_EQ(default_float_bounds, window2->GetBoundsInScreen());

  // Unfloat.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  // Window back to snapped state.
  EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            window_state2->GetStateType());
}

// Tests that manually resized floated bounds (by dragging the caption area) are
// restored across desk switches.
TEST_F(WindowFloatTest, RestoreResizeBounds) {
  auto* desks_controller = DesksController::Get();
  NewDesk();
  ASSERT_EQ(2u, desks_controller->desks().size());

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Drag the floated window away from its default bounds.
  auto* event_generator = GetEventGenerator();
  chromeos::HeaderView* header_view = GetHeaderView(window.get());
  event_generator->set_current_screen_location(
      header_view->GetBoundsInScreen().CenterPoint());
  event_generator->DragMouseTo(gfx::Point(100, 100));
  const gfx::Rect resize_bounds(window->bounds());

  // Switch to desk 2.
  ActivateDesk(desks_controller->desks()[1].get());
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_FALSE(window->IsVisible());

  // Switch back to desk 1. Test that the new floated bounds are restored.
  ActivateDesk(desks_controller->desks()[0].get());
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_TRUE(window->IsVisible());
  EXPECT_EQ(resize_bounds, window->bounds());

  // Test that minimizing and unminimizing also restores resized bounds.
  auto* window_state = WindowState::Get(window.get());
  window_state->Minimize();
  window_state->Restore();
  EXPECT_EQ(resize_bounds, window->bounds());
}

// Test that the float acclerator does not work on a non-floatable window.
TEST_F(WindowFloatTest, CantFloatAccelerator) {
  // Test window is NON_APP by default, which cannot be floated.
  auto window = CreateTestWindow();
  EXPECT_EQ(window.get(), window_util::GetActiveWindow());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(WindowState::Get(window.get())->IsFloated());
}

// Tests that after we drag a floated window to another display and then
// maximize, the window is on the correct display. Regression test for
// https://crbug.com/1360551.
TEST_F(WindowFloatTest, DragToOtherDisplayThenMaximize) {
  UpdateDisplay("1200x800,1201+0-1200x800");

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  ASSERT_EQ(Shell::GetAllRootWindows()[0], window->GetRootWindow());

  // Drag the window to the secondary display. Note that the event generator
  // does not update the display associated with the cursor, so we have to
  // manually do it here.
  chromeos::HeaderView* header_view = GetHeaderView(window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(
      header_view->GetBoundsInScreen().CenterPoint());
  const gfx::Point point(1600, 400);
  Shell::Get()->cursor_manager()->SetDisplay(
      display::Screen::GetScreen()->GetDisplayNearestPoint(point));
  event_generator->DragMouseTo(point);

  // Tests that the floated window is on the secondary display and remained
  // floated.
  ASSERT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow());
  WindowState* window_state = WindowState::Get(window.get());
  ASSERT_TRUE(window_state->IsFloated());

  // Maximize the window. Test that it stays on the secondary display.
  const WMEvent maximize(WM_EVENT_MAXIMIZE);
  window_state->OnWMEvent(&maximize);
  EXPECT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow());
}

// Tests that windows that are floated on non-primary displays are onscreen.
// Regression test for b/261860554.
TEST_F(WindowFloatTest, FloatOnOtherDisplay) {
  UpdateDisplay("1200x800,1201+0-1200x800");

  // Create a window on the secondary display.
  std::unique_ptr<aura::Window> window =
      CreateAppWindow(gfx::Rect(1200, 0, 300, 300));
  ASSERT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow());

  // After floating, the bounds of `window` should be full contained by the
  // secondary display bounds.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(
      gfx::Rect(1200, 0, 1200, 800).Contains(window->GetBoundsInScreen()));
}

// Tests that floated windows that are moved to an external display are still
// visible and the same size. Regression test for http://b/286864430 and
// http://b/297218727.
TEST_F(WindowFloatTest, MoveFloatedWindowToOtherDisplay) {
  // On two displays of the same size, this was never an issue. The issue
  // happened if the destination display was narrower than the source display.
  UpdateDisplay("1200x800,1201+0-600x800");

  // Create a floated window on the primary display. Upon floating, it will
  // automatically reposition to the bottom right corner. For this test, we want
  // to ensure the normal state size is different from the floated size; we do
  // this by initializing the window with a large size.
  auto window = CreateAppWindow(gfx::Rect(1100, 700));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  CHECK(WindowState::Get(window.get())->IsFloated());
  CHECK_NE(gfx::Size(1100, 700), window->bounds().size());

  const gfx::Size primary_display_size = window->bounds().size();

  // After moving the window to the secondary display, the bounds of `window`
  // should be partially visible and remain the same size.
  PressAndReleaseKey(ui::VKEY_M, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_EQ(Shell::GetAllRootWindows()[1], window->GetRootWindow());
  EXPECT_TRUE(Shell::GetAllRootWindows()[1]->GetBoundsInScreen().Intersects(
      window->GetBoundsInScreen()));
  EXPECT_EQ(primary_display_size, window->bounds().size());

  // After moving the window back to the primary display, the bounds of `window`
  // should be partially visible and remain the same size.
  PressAndReleaseKey(ui::VKEY_M, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_EQ(Shell::GetAllRootWindows()[0], window->GetRootWindow());
  EXPECT_TRUE(Shell::GetAllRootWindows()[0]->GetBoundsInScreen().Intersects(
      window->GetBoundsInScreen()));
  EXPECT_EQ(primary_display_size, window->bounds().size());
}

TEST_F(WindowFloatTest, FloatWindowBoundsWithZoomDisplay) {
  UpdateDisplay("1600x1000");

  // Create a floated window and position it on the top-right edge of the
  // display.
  std::unique_ptr<aura::Window> window =
      CreateAppWindow(gfx::Rect(1200, 0, 400, 300));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);

  // Use the accelerator to zoom the display up (ctrl + shift + "+") a couple
  // times. The floated window bounds should still be within the work area
  // bounds.
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
  PressAndReleaseKey(ui::VKEY_OEM_PLUS,
                     ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);

  EXPECT_TRUE(WorkAreaInsets::ForWindow(window.get())
                  ->user_work_area_bounds()
                  .Contains(window->GetBoundsInScreen()));
}

TEST_F(WindowFloatTest, FloatWindowBoundsWithShelfChange) {
  UpdateDisplay("1600x1000");

  // This test makes some assumptions that the shelf starts bottom aligned.
  ASSERT_EQ(ShelfAlignment::kBottom, GetPrimaryShelf()->alignment());

  // Create a floated window and position so it is semi offscreen.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  const gfx::Rect ideal_bounds(1400, 0, 400, 300);
  const SetBoundsWMEvent set_bounds_event(ideal_bounds);
  WindowState::Get(window.get())->OnWMEvent(&set_bounds_event);

  // Changing the shelf alignment should not alter the floated window bounds.
  GetPrimaryShelf()->SetAlignment(ShelfAlignment::kLeft);
  EXPECT_EQ(ideal_bounds, window->GetBoundsInScreen());
}

// Test float window per desk logic.
TEST_F(WindowFloatTest, OneFloatWindowPerDeskLogic) {
  // Test one float window per desk is allowed.
  auto* desks_controller = DesksController::Get();
  NewDesk();
  ASSERT_EQ(2u, desks_controller->desks().size());

  // Test creating floating window on different desk.
  // Float `window_1`.
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Move to desk 2.
  auto* desk_2 = desks_controller->desks()[1].get();
  ActivateDesk(desk_2);
  // `window_1` should not be visible since it's a different desk.
  EXPECT_FALSE(window_1->IsVisible());
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());
  // Both `window_1` and `window_2` should be floated since we allow one float
  // window per desk.
  EXPECT_TRUE(desk_2->is_active());
  EXPECT_TRUE(WindowState::Get(window_1.get())->IsFloated());
  EXPECT_TRUE(WindowState::Get(window_2.get())->IsFloated());
}

// Tests that `desks_util::BelongsToActiveDesk()` works as intended.
TEST_F(WindowFloatTest, BelongsToActiveDesk) {
  NewDesk();

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  EXPECT_TRUE(desks_util::BelongsToActiveDesk(window.get()));

  ActivateDesk(DesksController::Get()->desks()[1].get());
  EXPECT_FALSE(desks_util::BelongsToActiveDesk(window.get()));
}

// Test Desk removal for floating window.
TEST_F(WindowFloatTest, FloatWindowWithDeskRemoval) {
  auto* desks_controller = DesksController::Get();
  NewDesk();
  auto* desk_1 = desks_controller->desks()[0].get();
  auto* desk_2 = desks_controller->desks()[1].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Move to `desk 2`.
  ActivateDesk(desk_2);
  // Float `window_2` at `desk_2`.
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());

  // Verify `window_2` belongs to `desk_2`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_2.get()), desk_2);

  // Delete `desk_2` and `window_2` should be un-floated while `window_1` should
  // remain floated. As `window_1` is in the target desk and has higher
  // priority over `window_2` from the removed desk.
  RemoveDesk(desk_2);
  EXPECT_TRUE(WindowState::Get(window_1.get())->IsFloated());
  EXPECT_FALSE(WindowState::Get(window_2.get())->IsFloated());
  EXPECT_TRUE(window_1->IsVisible());
  EXPECT_TRUE(window_2->IsVisible());

  // Recreate `desk_2` without any floated window.
  NewDesk();
  desk_2 = desks_controller->desks()[1].get();
  ActivateDesk(desk_2);
  // Remove `desk_1` will move all windows from `desk_1` to the newly created
  // `desk_2` and the floated window should remain floated.
  RemoveDesk(desk_1);
  EXPECT_TRUE(WindowState::Get(window_1.get())->IsFloated());
}

// Test Close-all desk removal undo.
TEST_F(WindowFloatTest, FloatWindowWithDeskRemovalUndo) {
  // Test close all undo will restore float window.
  auto* desks_controller = DesksController::Get();
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  // Float `window` at `desk_2`.
  std::unique_ptr<aura::Window> window(CreateFloatedWindow());
  // Verify `window` belongs to `desk_2`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window.get()), desk_2);
  EnterOverview();
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
  RemoveDesk(desk_2, DeskCloseType::kCloseAllWindowsAndWait);
  ASSERT_TRUE(desk_2->is_desk_being_removed());
  // During desk removal, float window should be hidden.
  EXPECT_FALSE(window->IsVisible());
  // When `desk_2` is restored the floated window should remain floated and
  // shown.
  views::LabelButton* dismiss_button =
      DesksTestApi::GetCloseAllUndoToastDismissButton();
  LeftClickOn(dismiss_button);
  // Canceling close-all will bring the floated window back to shown.
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
  // Check if `window` still belongs to `desk_2`.
  ASSERT_EQ(float_controller->FindFloatedWindowOfDesk(desk_2), window.get());
  EXPECT_TRUE(window->IsVisible());

  // Commit desk removal should also delete the floated window on that desk.
  auto* window_widget = views::Widget::GetWidgetForNativeView(window.get());
  views::test::TestWidgetObserver observer(window_widget);
  // Release the unique_ptr for `window` here, as in close-all desk,
  // we will delete it below when the desk is removed. but since the unique_ptr
  // tries to delete it again, it will cause a crash in this test.
  window.release();
  RemoveDesk(desk_2, DeskCloseType::kCloseAllWindows);
  ASSERT_EQ(desks_controller->desks().size(), 1u);
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(observer.widget_closed());
}

// Test float window is included when building MRU window list.
TEST_F(WindowFloatTest, FloatWindowWithMRUWindowList) {
  auto* desks_controller = DesksController::Get();
  // Float `window_1` at `desk_1`.
  auto* desk_1 = desks_controller->desks()[0].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  // Float `window_2` at `desk_2`.
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());
  // Verify `window_2` belongs to `desk_2`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_2.get()), desk_2);
  // Move to `desk_1`.
  ActivateDesk(desk_1);
  // Calling MruWindowTracker::BuildMruWindowList(kActiveDesk) should return a
  // list that doesn't contain floated windows from inactive desk but contains
  // floated window on active desk.
  EXPECT_FALSE(desk_2->is_active());
  auto active_only_list =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
  EXPECT_TRUE(base::Contains(active_only_list, window_1.get()));
  EXPECT_FALSE(base::Contains(active_only_list, window_2.get()));
  // Calling MruWindowTracker::BuildMruWindowList(kAllDesks) should return a
  // list that contains all windows
  auto all_desks_mru_list =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kAllDesks);
  EXPECT_TRUE(base::Contains(all_desks_mru_list, window_1.get()));
  EXPECT_TRUE(base::Contains(all_desks_mru_list, window_2.get()));
}

// Test moving floating window between desks.
TEST_F(WindowFloatTest, MoveFloatWindowBetweenDesks) {
  auto* desks_controller = DesksController::Get();
  // Float `window_1` at `desk_1`.
  auto* desk_1 = desks_controller->desks()[0].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Verify `window_1` belongs to `desk_1`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_1);
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  // Float `window_2` at `desk_2`.
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());
  // Move back to `desk_1`.
  ActivateDesk(desk_1);
  auto* overview_controller = OverviewController::Get();
  EnterOverview();
  auto* overview_session = overview_controller->overview_session();
  // The window should exist on the grid of the first display.
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window_1.get());
  auto* grid =
      overview_session->GetGridWithRootWindow(Shell::GetPrimaryRootWindow());
  EXPECT_EQ(1u, grid->GetNumWindows());
  // Get position of `desk_2`'s desk mini view on the secondary display.
  const auto* desks_bar_view = grid->desks_bar_view();
  auto* desk_2_mini_view = desks_bar_view->mini_views()[1].get();
  gfx::Point desk_2_mini_view_center =
      desk_2_mini_view->GetBoundsInScreen().CenterPoint();

  // On overview, drag and drop floated `window_1` to `desk_2`.
  DragItemToPoint(overview_item, desk_2_mini_view_center, GetEventGenerator(),
                  /*by_touch_gestures=*/false,
                  /*drop=*/true);

  // Verify `window_1` belongs to `desk_2`.
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_2);
  // Verify `window_2` is unfloated.
  ASSERT_FALSE(WindowState::Get(window_2.get())->IsFloated());
}

// Test drag floating window between desks on different displays.
TEST_F(WindowFloatTest, MoveFloatWindowBetweenDesksOnDifferentDisplay) {
  UpdateDisplay("1000x400,1000+0-1000x400");
  auto* desks_controller = DesksController::Get();
  // Float `window_1` at `desk_1`.
  auto* desk_1 = desks_controller->desks()[0].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Verify `window_1` belongs to `desk_1`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_1);
  // Create `desk_2`.
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  // Float `window_2` at `desk_2`.
  std::unique_ptr<aura::Window> window_2(CreateFloatedWindow());
  // Move back to `desk_1`.
  ActivateDesk(desk_1);
  auto* overview_controller = OverviewController::Get();
  EnterOverview();
  auto* overview_session = overview_controller->overview_session();
  // Get root for displays.
  auto roots = Shell::GetAllRootWindows();
  ASSERT_EQ(2u, roots.size());
  aura::Window* primary_root = roots[0];
  aura::Window* secondary_root = roots[1];
  // The window should exist on the grid of the first display.
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window_1.get());
  auto* grid1 = overview_session->GetGridWithRootWindow(primary_root);
  auto* grid2 = overview_session->GetGridWithRootWindow(secondary_root);
  EXPECT_EQ(1u, grid1->GetNumWindows());
  EXPECT_EQ(grid1, overview_item->overview_grid());
  EXPECT_EQ(0u, grid2->GetNumWindows());

  // Get position of `desk_2`'s desk mini view on the secondary display.
  const auto* desks_bar_view = grid2->desks_bar_view();
  auto* desk_2_mini_view = desks_bar_view->mini_views()[1].get();
  gfx::Point desk_2_mini_view_center =
      desk_2_mini_view->GetBoundsInScreen().CenterPoint();

  // On overview, drag and drop floated `window_1` to `desk_2` on display 2.
  DragItemToPoint(overview_item, desk_2_mini_view_center, GetEventGenerator(),
                  /*by_touch_gestures=*/false,
                  /*drop=*/true);
  // Verify `window_2` is unfloated.
  ASSERT_FALSE(WindowState::Get(window_2.get())->IsFloated());
  // Verify `window_1` belongs to `desk_2`.
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_2);
  // Verify `window_1` belongs to display 2.
  EXPECT_TRUE(secondary_root->Contains(window_1.get()));
}

// Test that floated window are contained within the work area.
TEST_F(WindowFloatTest, FloatWindowWorkAreaConsiderations) {
  UpdateDisplay("1600x1000");

  // Create a window in the top right quadrant.
  std::unique_ptr<aura::Window> window =
      CreateAppWindow(gfx::Rect(1000, 100, 300, 300));

  // We will use the docked magnifier to modify the work area in this test.
  DockedMagnifierController* docked_magnifier_controller =
      Shell::Get()->docked_magnifier_controller();
  docked_magnifier_controller->SetEnabled(true);
  ASSERT_GT(docked_magnifier_controller->GetMagnifierHeightForTesting(), 0);

  // Float `window` and verify that it is underneath the docked magnifier
  // region.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_GT(window->GetBoundsInScreen().y(),
            docked_magnifier_controller->GetMagnifierHeightForTesting());

  // Enter tablet mode and drag the floated window so it magnitizes to the top
  // right. Verify that it is underneath the docked magnifier region.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  GetEventGenerator()->GestureScrollSequence(
      GetHeaderView(window.get())->GetBoundsInScreen().CenterPoint(),
      gfx::Point(1000, 300), base::Seconds(3),
      /*steps=*/10);
  EXPECT_GT(window->GetBoundsInScreen().y(),
            docked_magnifier_controller->GetMagnifierHeightForTesting());
}

// Tests that if we unminimize a window that was floated and another window has
// since been floated, unminimizing the window would not float it.
TEST_F(WindowFloatTest, UnminimizeWithFloatedWindow) {
  // Create two windows and float the second one and then minimize it.
  auto window1 = CreateAppWindow();
  auto window2 = CreateFloatedWindow();
  WindowState::Get(window2.get())->Minimize();

  ASSERT_EQ(window1.get(), window_util::GetActiveWindow());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window1.get())->IsFloated());

  // On unminimizing `window2`, we do not float it even if its pre-minimized
  // state is floated, as doing so would unfloat `window1`.
  WindowState::Get(window2.get())->Unminimize();
  EXPECT_TRUE(WindowState::Get(window1.get())->IsFloated());
}

// Test that floated window are not blocking keyboard events when it's on an
// inactive desk.
TEST_F(WindowFloatTest, FloatWindowShouldNotBlockKeyboardEvents) {
  auto* desks_controller = DesksController::Get();
  // Float `window_1` at `desk_1`.
  auto* desk_1 = desks_controller->desks()[0].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Verify `window_1` belongs to `desk_1`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_1);
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  // Going into overview mode from keyboard shortcut.
  auto* overview_controller = OverviewController::Get();
  ASSERT_FALSE(overview_controller->InOverviewSession());
  PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1, ui::EF_NONE);
  // Verify we are in overview mode.
  ASSERT_TRUE(overview_controller->InOverviewSession());
  // Repeat.
  PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1, ui::EF_NONE);
  ASSERT_FALSE(overview_controller->InOverviewSession());
  PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1, ui::EF_NONE);
  // Verify we are in overview mode.
  ASSERT_TRUE(overview_controller->InOverviewSession());
}

// Test that activate a floated window on an inactive desk will activate that
// desk.
TEST_F(WindowFloatTest, FloatWindowActivationActivatesBelongingDesk) {
  auto* desks_controller = DesksController::Get();
  // Float `window_1` at `desk_1`.
  auto* desk_1 = desks_controller->desks()[0].get();
  std::unique_ptr<aura::Window> window_1(CreateFloatedWindow());
  // Verify `window_1` belongs to `desk_1`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_1);
  NewDesk();
  auto* desk_2 = desks_controller->desks()[1].get();
  // Move to `desk_2`.
  ActivateDesk(desk_2);
  DeskSwitchAnimationWaiter waiter;
  // Activate `window_1` that belongs to `desk_1`.
  wm::ActivateWindow(window_1.get());
  EXPECT_TRUE(wm::IsActiveWindow(window_1.get()));
  waiter.Wait();
  // Verify `desk_1` is now activated.
  EXPECT_TRUE(desk_1->is_active());
}

// Tests that if a float window was activated before changing desks, it will be
// activated when returning to that desk.
TEST_F(WindowFloatTest, FloatWindowActivatesWhenChangingDesks) {
  auto* desks_controller = DesksController::Get();

  // Create a floated window on desk 1. We expect this window to be active later
  // when we return to desk 1.
  std::unique_ptr<aura::Window> floated_window1 = CreateFloatedWindow();
  ASSERT_TRUE(WindowState::Get(floated_window1.get())->IsActive());

  // Create a new desk with a floated window and a normal window. The normal
  // window should be activated.
  NewDesk();
  ActivateDesk(desks_controller->desks()[1].get());
  std::unique_ptr<aura::Window> floated_window2 = CreateFloatedWindow();
  std::unique_ptr<aura::Window> normal_window = CreateAppWindow();
  ASSERT_TRUE(WindowState::Get(normal_window.get())->IsActive());

  // Switch to desk 1, the first floated window should be active.
  ActivateDesk(desks_controller->desks()[0].get());
  EXPECT_TRUE(WindowState::Get(floated_window1.get())->IsActive());

  // Switch to desk 2, the normal window should be active.
  ActivateDesk(desks_controller->desks()[1].get());
  EXPECT_TRUE(WindowState::Get(normal_window.get())->IsActive());
}

// Test when we combine desks, floated window is updated on overview.
TEST_F(WindowFloatTest, FloatWindowUpdatedOnOverview) {
  auto* desks_controller = DesksController::Get();
  auto* desk_1 = desks_controller->desks()[0].get();
  // Float `window` at `desk_1`.
  std::unique_ptr<aura::Window> window(CreateFloatedWindow());
  // Verify `window` belongs to `desk_1`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window.get()), desk_1);
  NewDesk();
  ASSERT_EQ(desks_controller->desks().size(), 2u);
  EnterOverview();
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
  RemoveDesk(desk_1, DeskCloseType::kCombineDesks);
  ASSERT_EQ(desks_controller->desks().size(), 1u);
  // Floated window should be appended to overview items.
  const std::vector<std::unique_ptr<OverviewItemBase>>& overview_items =
      GetOverviewItemsForRoot(0);
  ASSERT_EQ(overview_items.size(), 1u);
  EXPECT_EQ(window.get(), overview_items[0]->GetWindow());
}

// Tests the floated window is hidden when there is a pinned window.
TEST_F(WindowFloatTest, PinnedWindow) {
  std::unique_ptr<aura::Window> floated_window = CreateFloatedWindow();

  // Create and pin a window. The floated window should be hidden.
  std::unique_ptr<aura::Window> pinned_window = CreateAppWindow();
  wm::ActivateWindow(pinned_window.get());
  window_util::PinWindow(pinned_window.get(), /*trusted=*/false);
  EXPECT_FALSE(floated_window->IsVisible());

  // Unpin the window.
  Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
      AcceleratorAction::kUnpin, {});
  EXPECT_TRUE(floated_window->IsVisible());

  // Trusted pin the window.
  window_util::PinWindow(pinned_window.get(), /*trusted=*/true);
  EXPECT_FALSE(floated_window->IsVisible());

  // Unpin the window by destroying it.
  pinned_window.reset();
  EXPECT_TRUE(floated_window->IsVisible());

  // Try pinning the floated window. It should still be visible.
  window_util::PinWindow(floated_window.get(), /*trusted=*/true);
  EXPECT_TRUE(floated_window->IsVisible());
}

// Tests that there is no crash when trying to float an always on top window.
// Regression test for b/279366443.
TEST_F(WindowFloatTest, AlwaysOnTopWindow) {
  std::unique_ptr<aura::Window> always_on_top_window = CreateAppWindow();
  always_on_top_window->SetProperty(aura::client::kZOrderingKey,
                                    ui::ZOrderLevel::kFloatingWindow);

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(WindowState::Get(always_on_top_window.get())->IsFloated());
}

// Tests that for unresizable windows, floatability depends on its window state
// type.
TEST_F(WindowFloatTest, UnresizableFloatPerWindowState) {
  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(600, 600));
  window->SetProperty(aura::client::kResizeBehaviorKey,
                      aura::client::kResizeBehaviorNone);
  auto* const window_state = WindowState::Get(window.get());

  // Unresizable freeform window should enter floating mode.
  WMEvent restore_event(WM_EVENT_NORMAL);
  window_state->OnWMEvent(&restore_event);
  ASSERT_TRUE(window_state->IsNormalStateType());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(window_state->IsFloated());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_FALSE(window_state->IsFloated());

  // Unresizable maximized window should not enter floating mode.
  WMEvent maximize_event(WM_EVENT_MAXIMIZE);
  window_state->OnWMEvent(&maximize_event);
  ASSERT_TRUE(window_state->IsMaximized());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(window_state->IsFloated());

  // Unresizable fullscreen window should not enter floating mode.
  WMEvent fullscreen_event(WM_EVENT_FULLSCREEN);
  window_state->OnWMEvent(&fullscreen_event);
  ASSERT_TRUE(window_state->IsFullscreen());
  window_state->Maximize();
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(window_state->IsFloated());
}

// Tests that a window sent to all desks can be floated.
TEST_F(WindowFloatTest, FloatAllDesksWindow) {
  // Create two new desks (three total).
  NewDesk();
  NewDesk();
  auto* desks_controller = DesksController::Get();
  ASSERT_EQ(3u, desks_controller->desks().size());

  // Create a floated window and a regular window on the first desk.
  auto first_floated_window = CreateFloatedWindow();
  auto all_desks_window = CreateAppWindow();

  // Assign the regular window to all desks.
  views::Widget::GetWidgetForNativeWindow(all_desks_window.get())
      ->SetVisibleOnAllWorkspaces(true);
  ASSERT_TRUE(
      desks_util::IsWindowVisibleOnAllWorkspaces(all_desks_window.get()));
  ASSERT_EQ(1u, desks_controller->visible_on_all_desks_windows().size());

  // Switch to the second desk and create another floated window.
  ActivateDesk(desks_controller->desks()[1].get());
  ASSERT_TRUE(wm::IsActiveWindow(all_desks_window.get()));
  auto second_floated_window = CreateFloatedWindow();

  // Switch to the third desk and float the `all_desks_window` using the
  // accelerator.
  ActivateDesk(desks_controller->desks()[2].get());
  ASSERT_TRUE(wm::IsActiveWindow(all_desks_window.get()));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(WindowState::Get(all_desks_window.get())->IsFloated());

  // Switch back to the first and second desks. The other floated windows should
  // no longer be floated, and the `all_desks_window` should still be floated.
  ActivateDesk(desks_controller->desks()[0].get());
  EXPECT_FALSE(WindowState::Get(first_floated_window.get())->IsFloated());
  EXPECT_TRUE(WindowState::Get(all_desks_window.get())->IsFloated());
  ActivateDesk(desks_controller->desks()[1].get());
  EXPECT_FALSE(WindowState::Get(second_floated_window.get())->IsFloated());
  EXPECT_TRUE(WindowState::Get(all_desks_window.get())->IsFloated());
}

// Tests that a floated window can be sent to all desks.
TEST_F(WindowFloatTest, SendFloatedWindowToAllDesks) {
  NewDesk();
  auto* desks_controller = DesksController::Get();
  ASSERT_EQ(2u, desks_controller->desks().size());

  // Create a floated window on both desks.
  auto first_floated_window = CreateFloatedWindow();
  ActivateDesk(desks_controller->desks()[1].get());
  auto all_floated_window = CreateFloatedWindow();

  // Assign the second floated window to all desks.
  views::Widget::GetWidgetForNativeWindow(all_floated_window.get())
      ->SetVisibleOnAllWorkspaces(true);
  ASSERT_EQ(1u, desks_controller->visible_on_all_desks_windows().size());
  ASSERT_TRUE(
      desks_util::IsWindowVisibleOnAllWorkspaces(all_floated_window.get()));
  ASSERT_TRUE(desks_util::BelongsToActiveDesk(all_floated_window.get()));

  // Switch back the first desk and verify `all_floated_window` is active
  // and floated.
  ActivateDesk(desks_controller->desks()[0].get());
  ASSERT_TRUE(desks_util::BelongsToActiveDesk(all_floated_window.get()));
  EXPECT_TRUE(WindowState::Get(all_floated_window.get())->IsFloated());
  // The first window should no longer be floated.
  ASSERT_TRUE(desks_util::BelongsToActiveDesk(first_floated_window.get()));
  EXPECT_FALSE(WindowState::Get(first_floated_window.get())->IsFloated());

  // Send `all_floated_window` to the second desk. It should no longer appear on
  // the first desk, but it should still be floated on the second desk.
  desks_controller->SendToDeskAtIndex(all_floated_window.get(), 1);
  ASSERT_FALSE(desks_util::BelongsToActiveDesk(all_floated_window.get()));
  ActivateDesk(desks_controller->desks()[1].get());
  ASSERT_TRUE(desks_util::BelongsToActiveDesk(all_floated_window.get()));
  EXPECT_TRUE(WindowState::Get(all_floated_window.get())->IsFloated());
}

// A test class that uses a mock time test environment.
class WindowFloatMetricsTest : public WindowFloatTest {
 public:
  WindowFloatMetricsTest()
      : WindowFloatTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  WindowFloatMetricsTest(const WindowFloatMetricsTest&) = delete;
  WindowFloatMetricsTest& operator=(const WindowFloatMetricsTest&) = delete;
  ~WindowFloatMetricsTest() override = default;

 protected:
  base::HistogramTester histogram_tester_;
};

// Tests the float window counts.
TEST_F(WindowFloatMetricsTest, FloatWindowCountPerSession) {
  // Float a window, Tests that it counts properly.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Unfloat and float the window again, it should count 2.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_FALSE(WindowState::Get(window.get())->IsFloated());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  // Float a new window on a new desk, it should count 3.
  NewDesk();
  std::unique_ptr<aura::Window> window_2 = CreateFloatedWindow();
  // Check total counts.
  EXPECT_EQ(FloatTestApi::GetFloatedWindowCounter(), 3);
}

// Tests the float window moved to another desk counts.
TEST_F(WindowFloatMetricsTest, FloatWindowMovedToAnotherDeskCountPerSession) {
  // Float a window, then move to another desk, tests that it counts properly.
  std::unique_ptr<aura::Window> window_1 = CreateFloatedWindow();
  // Create a new desk.
  NewDesk();
  auto* desks_controller = DesksController::Get();
  auto* desk_2 = desks_controller->desks()[1].get();
  EnterOverview();
  auto* overview_session = OverviewController::Get()->overview_session();
  // The window should exist on the grid of the first display.
  auto* overview_item =
      overview_session->GetOverviewItemForWindow(window_1.get());
  auto* grid =
      overview_session->GetGridWithRootWindow(Shell::GetPrimaryRootWindow());
  EXPECT_EQ(1u, grid->GetNumWindows());
  // Get position of `desk_2`'s desk mini view.
  const auto* desks_bar_view = grid->desks_bar_view();
  gfx::Point desk_2_mini_view_center =
      desks_bar_view->mini_views()[1]->GetBoundsInScreen().CenterPoint();

  // On overview, drag and drop floated `window_1` to `desk_2`.
  DragItemToPoint(overview_item, desk_2_mini_view_center, GetEventGenerator(),
                  /*by_touch_gestures=*/false,
                  /*drop=*/true);

  // Verify `window_1` belongs to `desk_2`.
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_EQ(float_controller->FindDeskOfFloatedWindow(window_1.get()), desk_2);
  // Check total counts, it should count 1.
  EXPECT_EQ(FloatTestApi::GetFloatedWindowMoveToAnotherDeskCounter(), 1);
  // Move to `desk_2` and remove `desk_2` by combine 2 desks.
  // Check total counts, it should count 2.
  ActivateDesk(desk_2);
  RemoveDesk(desk_2, DeskCloseType::kCombineDesks);
  EXPECT_EQ(FloatTestApi::GetFloatedWindowMoveToAnotherDeskCounter(), 2);
}

// Tests that the float window duration histogram is properly recorded.
TEST_F(WindowFloatMetricsTest, FloatWindowDuration) {
  constexpr char kHistogramName[] = "Ash.Float.FloatWindowDuration";

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

  // Float the window for 3 minutes and then maximize it. Tests that it records
  // properly.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  task_environment()->AdvanceClock(base::Minutes(3));
  task_environment()->RunUntilIdle();
  WindowState::Get(window.get())->Maximize();
  histogram_tester_.ExpectBucketCount(kHistogramName, 3, 1);

  // Float again for 3 hours. Test that it records into a different bucket.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  task_environment()->AdvanceClock(base::Hours(3));
  task_environment()->RunUntilIdle();
  WindowState::Get(window.get())->Maximize();
  histogram_tester_.ExpectBucketCount(kHistogramName, 180, 1);

  // Activate desk 2. At this point the floated window is still in floated
  // state, but is hidden, so we treat it as if a float session has ended.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  task_environment()->AdvanceClock(base::Minutes(3));
  task_environment()->RunUntilIdle();
  ActivateDesk(desks_controller->desks()[1].get());
  histogram_tester_.ExpectBucketCount(kHistogramName, 3, 2);

  // Activate desk 1. The floated window will be visible again and we start
  // recording the float session. Sending the window to desk 2 should record
  // the float duration.
  ActivateDesk(desks_controller->desks()[0].get());
  task_environment()->AdvanceClock(base::Minutes(3));
  task_environment()->RunUntilIdle();
  desks_controller->SendToDeskAtIndex(window.get(), 1);
  histogram_tester_.ExpectBucketCount(kHistogramName, 3, 3);

  // Activate desk 2. The floated window will be visible again and we start
  // recording the float session. Hiding and reshowing the window should not
  // record, this is to simulate hiding and reshowing while staying on the
  // active desk (i.e. showing the saved desks library).
  ActivateDesk(desks_controller->desks()[1].get());
  window->Hide();
  task_environment()->AdvanceClock(base::Minutes(3));
  task_environment()->RunUntilIdle();
  window->Show();
  histogram_tester_.ExpectBucketCount(kHistogramName, 3, 3);

  // Closing a window should record the float duration.
  window.reset();
  histogram_tester_.ExpectBucketCount(kHistogramName, 3, 4);
}

// Test bounds for a floated unresizable window.
TEST_F(WindowFloatTest, BoundsForUnresizableWindow) {
  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(600, 600));
  window->SetProperty(aura::client::kResizeBehaviorKey,
                      aura::client::kResizeBehaviorNone);
  const gfx::Size window_size = window->GetBoundsInScreen().size();

  // Float the window. Test that the size does not change, and that it is
  // roughly in the bottom right.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  const gfx::Rect work_area_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
  const gfx::Rect window_bounds = window->GetBoundsInScreen();
  EXPECT_EQ(window_size, window_bounds.size());
  EXPECT_NEAR(window_bounds.bottom(), work_area_bounds.bottom(), 10);
  EXPECT_NEAR(window_bounds.right(), work_area_bounds.right(), 10);
}

class TabletWindowFloatTest : public WindowFloatTest,
                              public display::DisplayObserver {
 public:
  TabletWindowFloatTest() = default;
  TabletWindowFloatTest(const TabletWindowFloatTest&) = delete;
  TabletWindowFloatTest& operator=(const TabletWindowFloatTest&) = delete;
  ~TabletWindowFloatTest() override = default;

  // Flings a window in the direction provided by `left` and `up`.
  void FlingWindow(aura::Window* window, bool left, bool up) {
    const gfx::Point start =
        GetHeaderView(window)->GetBoundsInScreen().CenterPoint();
    const gfx::Vector2d offset(left ? -10 : 10, up ? -10 : 10);
    GetEventGenerator()->GestureScrollSequence(
        start, start + offset, base::Milliseconds(10), /*steps=*/1);
  }

  // Drags `window` so that it magnetizes to `corner`.
  void MagnetizeWindow(aura::Window* window,
                       FloatController::MagnetismCorner corner) {
    const gfx::Rect area =
        WorkAreaInsets::ForWindow(window)->user_work_area_bounds();

    gfx::Point end;
    switch (corner) {
      case FloatController::MagnetismCorner::kTopLeft:
        end = area.origin();
        break;
      case FloatController::MagnetismCorner::kBottomLeft:
        end = area.bottom_left();
        break;
      case FloatController::MagnetismCorner::kTopRight:
        end = area.top_right();
        break;
      case FloatController::MagnetismCorner::kBottomRight:
        end = area.bottom_right();
        break;
    }
    GetEventGenerator()->GestureScrollSequence(
        GetHeaderView(window)->GetBoundsInScreen().CenterPoint(), end,
        base::Milliseconds(100), /*steps=*/3);
  }

  // Checks that `window` has been magnetized in `corner`.
  void CheckMagnetized(aura::Window* window,
                       FloatController::MagnetismCorner corner) {
    const gfx::Rect work_area =
        WorkAreaInsets::ForWindow(window)->user_work_area_bounds();
    const int padding = chromeos::wm::kFloatedWindowPaddingDp;
    switch (corner) {
      case FloatController::MagnetismCorner::kTopLeft:
        EXPECT_EQ(gfx::Point(padding, padding), window->bounds().origin());
        return;
      case FloatController::MagnetismCorner::kBottomLeft:
        EXPECT_EQ(gfx::Point(padding, work_area.bottom() - padding),
                  window->bounds().bottom_left());
        return;
      case FloatController::MagnetismCorner::kTopRight:
        EXPECT_EQ(gfx::Point(work_area.right() - padding, padding),
                  window->bounds().top_right());
        return;
      case FloatController::MagnetismCorner::kBottomRight:
        EXPECT_EQ(gfx::Point(work_area.right() - padding,
                             work_area.bottom() - padding),
                  window->bounds().bottom_right());
        return;
    }
  }

  void SetOnTabletStateChangedCallback(
      base::RepeatingCallback<void(display::TabletState)> callback) {
    on_tablet_state_changed_callback_ = callback;
    display_observer_.emplace(this);
  }

  // WindowFloatTest:
  void SetUp() override {
    // This allows us to snap to the bottom in portrait mode.
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        ::switches::kUseFirstDisplayAsInternal);
    WindowFloatTest::SetUp();
  }

  void TearDown() override {
    display_observer_.reset();
    WindowFloatTest::TearDown();
  }

  // display::DisplayObserver:
  void OnDisplayTabletStateChanged(display::TabletState state) override {
    on_tablet_state_changed_callback_.Run(state);
  }

 protected:
  base::UserActionTester user_action_tester_;

 private:
  // Called when the tablet state changes.
  base::RepeatingCallback<void(display::TabletState)>
      on_tablet_state_changed_callback_;

  std::optional<display::ScopedDisplayObserver> display_observer_;
};

// Test class used to keep track of the amount of times the tuck education nudge
// has appeared.
class NudgeCounter : public aura::WindowObserver {
 public:
  NudgeCounter() {
    window_observation_.Observe(Shell::Get()->GetPrimaryRootWindow());
  }
  NudgeCounter(const NudgeCounter&) = delete;
  NudgeCounter& operator=(const NudgeCounter&) = delete;
  ~NudgeCounter() override = default;

  int nudge_count() const { return nudge_count_; }

  // aura::WindowObserver:
  void OnWindowHierarchyChanged(const HierarchyChangeParams& params) override {
    if (params.target->GetName() == "TuckEducationNudgeWidget" &&
        params.new_parent) {
      nudge_count_++;
    }
  }
  void OnWindowDestroying(aura::Window* window) override {
    window_observation_.Reset();
  }

 private:
  int nudge_count_ = 0;

  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};
};

TEST_F(TabletWindowFloatTest, TabletClamshellTransition) {
  auto window1 = CreateFloatedWindow();

  // Test that on entering tablet mode, we maintain float state.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  EXPECT_TRUE(WindowState::Get(window1.get())->IsFloated());

  // Create a new floated window in tablet mode. It should unfloat the existing
  // floated window.
  auto window2 = CreateFloatedWindow();
  EXPECT_FALSE(WindowState::Get(window1.get())->IsFloated());

  // Test that on exiting tablet mode, we maintain float state.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
  EXPECT_TRUE(WindowState::Get(window2.get())->IsFloated());
}

TEST_F(TabletWindowFloatTest, ClamshellToTabletMagnetism) {
  auto window = CreateFloatedWindow();
  window->SetBounds(gfx::Rect(300, 300));

  // Verify that on entering tablet mode, since our window's origin was 0,0, the
  // window is magnetized to the top left.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopLeft);
}

// Tests that the expected windows are animating duration a tablet <-> clamshell
// transition.
TEST_F(TabletWindowFloatTest, TabletClamshellTransitionAnimation) {
  auto normal_window = CreateAppWindow();
  auto floated_window = CreateFloatedWindow();

  // Both windows are expected to animate, so we wait for them both. We don't
  // know which window would finish first so we wait for the normal window
  // first, and if the floated window is still animating, wait for that as well.
  auto wait_for_windows_finish_animating = [&]() {
    ShellTestApi().WaitForWindowFinishAnimating(normal_window.get());
    if (floated_window->layer()->GetAnimator()->is_animating())
      ShellTestApi().WaitForWindowFinishAnimating(floated_window.get());
  };

  // Activate `normal_window`. `floated_window` should still animate since it is
  // stacked on top and visible.
  wm::ActivateWindow(normal_window.get());

  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Tests that on entering tablet mode, both windows are animating since both
  // are visible before and after the transition.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  ASSERT_TRUE(IsVisiblyAnimating(normal_window.get()));
  ASSERT_TRUE(IsVisiblyAnimating(floated_window.get()));
  wait_for_windows_finish_animating();

  // Tests that both windows are animating on exit.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
  ASSERT_TRUE(IsVisiblyAnimating(normal_window.get()));
  ASSERT_TRUE(IsVisiblyAnimating(floated_window.get()));
  wait_for_windows_finish_animating();

  // Activate `floated_window`. `normal_window` should still animate since it
  // is visible behind `floated_window`.
  wm::ActivateWindow(floated_window.get());

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  ASSERT_TRUE(IsVisiblyAnimating(normal_window.get()));
  ASSERT_TRUE(IsVisiblyAnimating(floated_window.get()));
  wait_for_windows_finish_animating();

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
  EXPECT_TRUE(IsVisiblyAnimating(normal_window.get()));
  EXPECT_TRUE(IsVisiblyAnimating(floated_window.get()));
}

// Tests that the new minimum size is respected when entering tablet mode.
// Regression test for b/261780362.
TEST_F(TabletWindowFloatTest, MinimumSizeChangeOnTablet) {
  UpdateDisplay("800x600");

  // Create a window in clamshell mode without a minimum size, and larger than
  // its tablet minimum size.
  auto window =
      CreateAppWindow(gfx::Rect(500, 500), chromeos::AppType::SYSTEM_APP,
                      kShellWindowId_DeskContainerA, new TestWidgetDelegateAsh);
  auto* custom_frame = static_cast<TestNonClientFrameViewAsh*>(
      NonClientFrameViewAsh::Get(window.get()));
  wm::ActivateWindow(window.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

  // Set a minimum size for tablet that is floatable.
  const gfx::Size tablet_minimum_size(300, 300);

  // Change the minimum size when tablet mode is changed. This mimics the
  // behaviour chrome windows have, when they switch to a more tap friendly mode
  // with larger buttons, which results in a larger minimum size.
  SetOnTabletStateChangedCallback(
      base::BindLambdaForTesting([&](display::TabletState state) {
        switch (state) {
          case display::TabletState::kInTabletMode:
            custom_frame->SetMinimumSize(tablet_minimum_size);
            return;
          case display::TabletState::kInClamshellMode:
            custom_frame->SetMinimumSize(gfx::Size());
            return;
          case display::TabletState::kEnteringTabletMode:
          case display::TabletState::kExitingTabletMode:
            break;
        }
      }));

  // Tests that on entering tablet mode, the window is sized to its minimum
  // width.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_EQ(tablet_minimum_size.width(), window->bounds().width());

  // Alter the minimum size of the window. Tests that the window bounds adjust
  // to match.
  custom_frame->SetMinimumSize(gfx::Size(350, 350));
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_EQ(350, window->bounds().width());
}

// Tests if a browser can float at several minimum sizes.
TEST_F(TabletWindowFloatTest, CanBrowsersFloat) {
  UpdateDisplay("800x600");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  const int work_area_width =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area().width();
  const int maximum_float_width =
      (work_area_width - chromeos::wm::kSplitviewDividerShortSideLength) / 2 -
      chromeos::wm::kFloatedWindowPaddingDp * 2;

  aura::test::TestWindowDelegate window_delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &window_delegate, /*id=*/-1, gfx::Rect(500, 500)));
  window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
  wm::ActivateWindow(window.get());

  // Browser windows whose minimum size is greater than the maximum allowed
  // float width can never be floated.
  window_delegate.set_minimum_size(gfx::Size(maximum_float_width + 5, 100));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_FALSE(WindowState::Get(window.get())->IsFloated());

  // Browser windows whose minimum size is slightly less than the maximum
  // allowed, will be floated and have some extra padding, but their size should
  // not exceed the maximum allowed.
  window_delegate.set_minimum_size(gfx::Size(maximum_float_width - 20, 100));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_EQ(maximum_float_width, window->bounds().width());

  // Unfloat the window for the next check.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_FALSE(WindowState::Get(window.get())->IsFloated());

  // Browser windows whose minimum size is smaller that the maximum allowed will
  // have extra padding to make the omnibox tappable.
  window_delegate.set_minimum_size(gfx::Size(250, 100));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_EQ(250 + chromeos::wm::kBrowserExtraPaddingDp,
            window->bounds().width());
}

// Tests that a window can be floated in tablet mode, unless its minimum width
// is greater than half the work area.
TEST_F(TabletWindowFloatTest, TabletPositioningLandscape) {
  UpdateDisplay("800x600");

  aura::test::TestWindowDelegate window_delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &window_delegate, /*id=*/-1, gfx::Rect(300, 300)));
  window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
  wm::ActivateWindow(window.get());

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_FALSE(WindowState::Get(window.get())->IsFloated());

  window_delegate.set_minimum_size(gfx::Size(600, 600));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(WindowState::Get(window.get())->IsFloated());
}

// Tests that a window that cannot be floated in tablet mode unfloats after
// entering tablet mode.
TEST_F(TabletWindowFloatTest, FloatWindowUnfloatsEnterTablet) {
  UpdateDisplay("800x600");

  aura::test::TestWindowDelegate window_delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &window_delegate, /*id=*/-1, gfx::Rect(850, 850)));
  window_delegate.set_minimum_size(gfx::Size(500, 500));
  window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
  wm::ActivateWindow(window.get());

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  EXPECT_FALSE(WindowState::Get(window.get())->IsFloated());
}

// Tests that a floated window unfloats if a display change makes it no longer a
// valid floating window.
TEST_F(TabletWindowFloatTest, FloatWindowUnfloatsDisplayChange) {
  UpdateDisplay("1800x1000");

  aura::test::TestWindowDelegate window_delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &window_delegate, /*id=*/-1, gfx::Rect(300, 300)));
  window_delegate.set_minimum_size(gfx::Size(400, 400));
  window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
  wm::ActivateWindow(window.get());

  // Enter tablet mode and float `window`.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

  // If the display width is 700, the minimum width exceeds half the display
  // width.
  UpdateDisplay("700x600");
  EXPECT_FALSE(WindowState::Get(window.get())->IsFloated());
}

// Tests that windows floated in tablet mode have immersive mode disabled,
// showing their title bars.
TEST_F(TabletWindowFloatTest, ImmersiveMode) {
  // Create a test app window that has a header.
  auto window = CreateAppWindow();
  auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get(
      views::Widget::GetWidgetForNativeView(window.get()));

  // Enter tablet mode and float `window`.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  EXPECT_TRUE(immersive_controller->IsEnabled());

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(immersive_controller->IsEnabled());

  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(immersive_controller->IsEnabled());

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
  EXPECT_FALSE(immersive_controller->IsEnabled());
}

// Tests that if we minimize a floated window, it is floated upon unminimizing,
// and magnetized in the same corner as before.
TEST_F(TabletWindowFloatTest, UnminimizeFloatWindow) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kTopLeft);

  auto* window_state = WindowState::Get(window.get());
  window_state->Minimize();
  window_state->Unminimize();
  EXPECT_TRUE(window_state->IsFloated());
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopLeft);
}

TEST_F(TabletWindowFloatTest, Rotation) {
  // Use a display where the width and height are quite different, otherwise it
  // would be hard to tell if portrait mode is using landscape bounds to
  // calculate floating window bounds.
  UpdateDisplay("1800x1000");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  const gfx::Rect no_rotation_bounds = window->bounds();

  // First rotate to landscape secondary orientation. The float bounds should
  // be the same.
  ScreenOrientationControllerTestApi orientation_test_api(
      Shell::Get()->screen_orientation_controller());
  orientation_test_api.SetDisplayRotation(
      display::Display::ROTATE_180, display::Display::RotationSource::ACTIVE);
  EXPECT_EQ(window->bounds(), no_rotation_bounds);

  // Rotate to the two portrait orientations. The float bounds should be
  // similar since landscape bounds are used for portrait float calculations
  // as well, but slightly different since the shelf affects the work area
  // differently.
  const int shelf_size = ShelfConfig::Get()->shelf_size();
  orientation_test_api.SetDisplayRotation(
      display::Display::ROTATE_90, display::Display::RotationSource::ACTIVE);
  EXPECT_NEAR(no_rotation_bounds.width(), window->bounds().width(), shelf_size);
  EXPECT_NEAR(no_rotation_bounds.height(), window->bounds().height(),
              shelf_size);

  orientation_test_api.SetDisplayRotation(
      display::Display::ROTATE_270, display::Display::RotationSource::ACTIVE);
  EXPECT_NEAR(no_rotation_bounds.width(), window->bounds().width(), shelf_size);
  EXPECT_NEAR(no_rotation_bounds.height(), window->bounds().height(),
              shelf_size);
}

// Tests that dragged float window follows the touch. Regression test for
// https://crbug.com/1362727.
TEST_F(TabletWindowFloatTest, Dragging) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Start dragging in the center of the header. When moving the touch, the
  // header should move with the touch such that the touch remains in the center
  // of the header.
  chromeos::HeaderView* header_view = GetHeaderView(window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->PressTouch(header_view->GetBoundsInScreen().CenterPoint());

  // Drag to several points. Verify the header is aligned with the new touch
  // point. Also verify that we do not recreate the window while dragging - this
  // would mean we are using a cross-fade animation. See b/272529481 for more
  // details.
  WindowLayerRecreatedCounter recreated_counter(window.get());
  for (const gfx::Point& point :
       {gfx::Point(10, 10), gfx::Point(100, 10), gfx::Point(400, 400)}) {
    event_generator->MoveTouch(point);
    EXPECT_EQ(point, header_view->GetBoundsInScreen().CenterPoint());
  }
  EXPECT_EQ(0, recreated_counter.recreated_count());
}

// Tests that there is no crash when maximizing a dragged floated window.
// Regression test for http://b/254107825.
TEST_F(TabletWindowFloatTest, MaximizeWhileDragging) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Press the accelerator to maximize before releasing touch.
  chromeos::HeaderView* header_view = GetHeaderView(window.get());
  auto* event_generator = GetEventGenerator();
  event_generator->PressTouch(header_view->GetBoundsInScreen().CenterPoint());
  event_generator->MoveTouch(gfx::Point(100, 100));
  PressAndReleaseKey(ui::VKEY_OEM_PLUS, ui::EF_ALT_DOWN);

  EXPECT_TRUE(WindowState::Get(window.get())->IsMaximized());

  // Press the accelerator to minimize before releasing touch.
  event_generator->PressTouch(header_view->GetBoundsInScreen().CenterPoint());
  event_generator->MoveTouch(gfx::Point(100, 100));
  PressAndReleaseKey(ui::VKEY_OEM_MINUS, ui::EF_ALT_DOWN);

  EXPECT_TRUE(WindowState::Get(window.get())->IsMinimized());
}

// Tests that on drag release, the window sticks to one of the four corners of
// the work area.
TEST_F(TabletWindowFloatTest, DraggingMagnetism) {
  // Use a set display size so we can drag to specific spots.
  UpdateDisplay("1600x1000");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  const int padding = chromeos::wm::kFloatedWindowPaddingDp;
  const int shelf_size = ShelfConfig::Get()->shelf_size();

  // The default location is in the bottom right.
  EXPECT_EQ(gfx::Point(1600 - padding, 1000 - padding - shelf_size),
            window->bounds().bottom_right());
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);

  // Test no change if we drag it in the bottom right.
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kBottomRight);
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);

  // Move finger somewhere in the top right, but not too right that it falls
  // into the snap region. Test that on release, it magnetizes to the top right
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kTopRight);
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopRight);

  // Move finger to somewhere in the top left, but not too left that it falls
  // into the snap region. Test that on release, it magnetizes to the top left.
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kTopLeft);
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopLeft);

  // Switch to portrait orientation and move finger somewhere in the bottom
  // left, but not too bottom that it falls into the snap region. Test that on
  // release, it magentizes to the bottom left.
  UpdateDisplay("1000x1600");
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kBottomLeft);
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomLeft);
}

TEST_F(TabletWindowFloatTest, UntuckWindowOnExitTabletMode) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // The window is magnetized to the bottom right by default.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Fling to tuck the window in the bottom right.
  auto* float_controller = Shell::Get()->float_controller();
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Tests that after we exit tablet mode, the window is untucked and fully
  // visible, but is still floated.
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
  EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  EXPECT_TRUE(screen_util::GetDisplayBoundsInParent(window.get())
                  .Contains(window->bounds()));
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
}

TEST_F(TabletWindowFloatTest, UntuckWindowOnActivation) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Fling to tuck the window in the bottom right.
  auto* float_controller = Shell::Get()->float_controller();
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Tests that after we activate the window, the window is untucked and fully
  // visible, but is still floated.
  wm::ActivateWindow(window.get());
  EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  EXPECT_TRUE(screen_util::GetDisplayBoundsInParent(window.get())
                  .Contains(window->bounds()));
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
}

// Tests that when we switch desks, a tucked window gets untucked.
TEST_F(TabletWindowFloatTest, UntuckWindowOnDeskChange) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  auto* desks_controller = DesksController::Get();
  NewDesk();
  ASSERT_EQ(2u, desks_controller->desks().size());
  ActivateDesk(desks_controller->desks()[1].get());

  // Create a floated window on the second desk and fling it.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  const gfx::Rect pre_tucked_bounds = window->bounds();
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(desks_util::BelongsToActiveDesk(window.get()));
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_TRUE(Shell::Get()->float_controller()->IsFloatedWindowTuckedForTablet(
      window.get()));

  // Activate desk 1. The window is still floated but is hidden and not tucked.
  ActivateDesk(desks_controller->desks()[0].get());
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
  EXPECT_FALSE(window->IsVisible());
  EXPECT_EQ(pre_tucked_bounds, window->bounds());
  EXPECT_FALSE(Shell::Get()->float_controller()->IsFloatedWindowTuckedForTablet(
      window.get()));
}

// Tests that the tucked window is invisible while it is fully tucked.
TEST_F(TabletWindowFloatTest, TuckedWindowVisibility) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Fling to tuck the window in the bottom right. Test that the window is
  // invisible once the animation is finished.
  auto* float_controller = Shell::Get()->float_controller();
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  EXPECT_TRUE(window->IsVisible());
  ShellTestApi().WaitForWindowFinishAnimating(window.get());
  EXPECT_FALSE(window->IsVisible());
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Tests that there is an overview item created for the tucked window.
  ToggleOverview();
  WaitForOverviewEnterAnimation();
  EXPECT_TRUE(GetOverviewItemForWindow(window.get()));

  // Tests that after we activate the window, the window is visible again as it
  // is getting untucked.
  wm::ActivateWindow(window.get());
  ShellTestApi().WaitForWindowFinishAnimating(window.get());
  EXPECT_TRUE(window->IsVisible());
}

// Tests that the floated window is visible when it is untucked.
TEST_F(TabletWindowFloatTest, UntuckedWindowVisibility) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Long duration for tuck animation, to allow it to be interrupted.
  ui::ScopedAnimationDurationScaleMode test_duration(
      ui::ScopedAnimationDurationScaleMode::SLOW_DURATION);

  // Fling to tuck the window. Press the untuck handle before the window
  // finishes the tuck animation. Test that the window is visible.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  auto* float_controller = Shell::Get()->float_controller();
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  ASSERT_TRUE(tuck_handle_widget);
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_TRUE(window->IsVisible());
  EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
}

// Tests that the expected window gets activation after tucking a floated
// window, and that on untucking the floated window, it gains activation.
TEST_F(TabletWindowFloatTest, WindowActivationAfterTuckingUntucking) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> float_window = CreateFloatedWindow();

  // Fling to tuck the window in the bottom right.
  ASSERT_EQ(float_window.get(), window_util::GetActiveWindow());
  FlingWindow(float_window.get(), /*left=*/false, /*up=*/false);
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_TRUE(
      float_controller->IsFloatedWindowTuckedForTablet(float_window.get()));

  // There are no other app windows, so the activation goes to the app list.
  EXPECT_EQ(Shell::Get()->app_list_controller()->GetWindow(),
            window_util::GetActiveWindow());

  // Create another window and untuck the floated window.
  std::unique_ptr<aura::Window> window2 = CreateAppWindow();
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(float_window.get());
  ASSERT_TRUE(tuck_handle_widget);
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(
      float_controller->IsFloatedWindowTuckedForTablet(float_window.get()));
  ASSERT_EQ(float_window.get(), window_util::GetActiveWindow());

  // Tests that tucking the floated window activates the window underneath.
  FlingWindow(float_window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(
      float_controller->IsFloatedWindowTuckedForTablet(float_window.get()));
  EXPECT_EQ(window2.get(), window_util::GetActiveWindow());

  // Untuck the floated window and minimize the other window.
  tuck_handle_widget =
      float_controller->GetTuckHandleWidget(float_window.get());
  ASSERT_TRUE(tuck_handle_widget);
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(
      float_controller->IsFloatedWindowTuckedForTablet(float_window.get()));
  WindowState::Get(window2.get())->Minimize();
  ASSERT_EQ(float_window.get(), window_util::GetActiveWindow());

  // Tests that tucking the floated window activates the app list instead of
  // activating and unminimizing the minimized window.
  FlingWindow(float_window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(
      float_controller->IsFloatedWindowTuckedForTablet(float_window.get()));
  EXPECT_EQ(Shell::Get()->app_list_controller()->GetWindow(),
            window_util::GetActiveWindow());
}

// Tests the functionality of tucking a window in tablet mode.
TEST_F(TabletWindowFloatTest, TuckWindowLeft) {
  UpdateDisplay("1600x1000");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Magnetize the window to the top left.
  MagnetizeWindow(window.get(), FloatController::MagnetismCorner::kTopLeft);
  auto* float_controller = Shell::Get()->float_controller();

  // Verify user action initial states.
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kTuckUserAction));
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kUntuckUserAction));

  // Fling the window left and up. Test that it tucks in the top left.
  FlingWindow(window.get(), /*left=*/true, /*up=*/true);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  const int padding = chromeos::wm::kFloatedWindowPaddingDp;
  EXPECT_EQ(gfx::Point(0, padding), window->bounds().top_right());
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kTuckUserAction));

  // Test that the tuck handle is aligned with the window.
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  ASSERT_TRUE(tuck_handle_widget);
  EXPECT_EQ(window->bounds().right_center(),
            tuck_handle_widget->GetWindowBoundsInScreen().left_center());

  // Untuck the window. Test that it magnetizes to the top left.
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopLeft);
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kUntuckUserAction));

  // Fling the window left and down. Test that it tucks in the bottom left.
  FlingWindow(window.get(), /*left=*/true, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  const gfx::Rect work_area = WorkAreaInsets::ForWindow(window->GetRootWindow())
                                  ->user_work_area_bounds();
  EXPECT_EQ(gfx::Point(0, work_area.bottom() - padding),
            window->bounds().bottom_right());
  EXPECT_EQ(2, user_action_tester_.GetActionCount(kTuckUserAction));

  // Untuck the window. Test that it magnetizes to the bottom left.
  tuck_handle_widget = float_controller->GetTuckHandleWidget(window.get());
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomLeft);
  EXPECT_EQ(2, user_action_tester_.GetActionCount(kUntuckUserAction));
}

// Tests the functionality of tucking a window in tablet mode.
TEST_F(TabletWindowFloatTest, TuckWindowRight) {
  UpdateDisplay("1600x1000");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // The window is magnetized to the bottom right by default.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  auto* float_controller = Shell::Get()->float_controller();
  const gfx::Rect work_area = WorkAreaInsets::ForWindow(window->GetRootWindow())
                                  ->user_work_area_bounds();

  // Verify user action initial states.
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kTuckUserAction));
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kUntuckUserAction));

  // Fling the window right and up. Test that it tucks in the top right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/true);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  const int padding = chromeos::wm::kFloatedWindowPaddingDp;
  EXPECT_EQ(gfx::Point(work_area.right(), padding), window->bounds().origin());
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kTuckUserAction));

  // Test that the tuck handle is aligned with the window.
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  ASSERT_TRUE(tuck_handle_widget);
  EXPECT_EQ(window->bounds().left_center(),
            tuck_handle_widget->GetWindowBoundsInScreen().right_center());

  // Untuck the window. Test that it magnetizes to the top right.
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopRight);
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kUntuckUserAction));

  // Fling the window right and down. Test that it tucks in the bottom right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  EXPECT_EQ(gfx::Point(work_area.right(), work_area.bottom() - padding),
            window->bounds().bottom_left());
  EXPECT_EQ(2, user_action_tester_.GetActionCount(kTuckUserAction));

  // Untuck the window. Test that it magnetizes to the bottom right.
  tuck_handle_widget = float_controller->GetTuckHandleWidget(window.get());
  GestureTapOn(tuck_handle_widget->GetContentsView());
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);
  EXPECT_EQ(2, user_action_tester_.GetActionCount(kUntuckUserAction));
}

// Tests that the window gets tucked to the closer edge and corner based on the
// fling velocity.
TEST_F(TabletWindowFloatTest, TuckToMagnetismCorner) {
  UpdateDisplay("1600x1000");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  // Create a floated window in the bottom right.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);

  auto* float_controller = Shell::Get()->float_controller();

  // Fling the window left and up. Test that it does not tuck but magnetizes to
  // the top left.
  FlingWindow(window.get(), /*left=*/true, /*up=*/true);
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopLeft);

  // Fling the window left and down. Now the window should tuck in the bottom
  // left.
  FlingWindow(window.get(), /*left=*/true, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  const gfx::Rect work_area = WorkAreaInsets::ForWindow(window->GetRootWindow())
                                  ->user_work_area_bounds();
  const int padding = chromeos::wm::kFloatedWindowPaddingDp;
  EXPECT_EQ(gfx::Point(0, work_area.bottom() - padding),
            window->bounds().bottom_right());
}

// Tests that tapping on a point on the edge but far from the tuck handle does
// not untuck a tucked window. Regression test for b/262573071.
TEST_F(TabletWindowFloatTest, TapOnEdgeDoesNotUntuck) {
  UpdateDisplay("800x600");

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // The window is magnetized to the bottom right by default.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  auto* float_controller = Shell::Get()->float_controller();

  // Tuck the window in the bottom right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Select a point close to the edge that is not close to the tuck handle.
  const gfx::Point point(799, window->GetBoundsInScreen().y());
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  ASSERT_FALSE(tuck_handle_widget->GetWindowBoundsInScreen().Contains(point));

  // Tests that we are still tucked after tapping that point.
  GetEventGenerator()->GestureTapAt(point);
  EXPECT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
}

// Tests that the tuck handle tap target is larger than its bounds.
TEST_F(TabletWindowFloatTest, TuckHandleTapTarget) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  auto* float_controller = Shell::Get()->float_controller();

  // Tuck the window in the bottom right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Tap somewhere slightly outside the tuck handle widget. Verify that the
  // tucked window is untucked.
  const gfx::Rect tuck_handle_bounds =
      float_controller->GetTuckHandleWidget(window.get())
          ->GetWindowBoundsInScreen();
  GetEventGenerator()->GestureTapAt(tuck_handle_bounds.origin() -
                                    gfx::Vector2d(5, 5));
  EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
}

// Tests that the tuck handle is offscreen in overview mode.
TEST_F(TabletWindowFloatTest, TuckHandleOffscreenInOverview) {
  const gfx::Rect display_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().bounds();

  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  auto* float_controller = Shell::Get()->float_controller();

  // Tuck the window in the bottom right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // The tuck handle widget is normally onscreen.
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  EXPECT_TRUE(
      display_bounds.Contains(tuck_handle_widget->GetWindowBoundsInScreen()));

  // Tests that on entering overview, the tuck handle is offscreen.
  EnterOverview();
  EXPECT_FALSE(
      display_bounds.Contains(tuck_handle_widget->GetWindowBoundsInScreen()));

  // Tests that on leaving overview, the tuck handle is onscreen again.
  ExitOverview();
  EXPECT_TRUE(
      display_bounds.Contains(tuck_handle_widget->GetWindowBoundsInScreen()));
}

TEST_F(TabletWindowFloatTest, UntuckWindowGestures) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // The window is magnetized to the bottom right by default.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();
  auto* float_controller = Shell::Get()->float_controller();

  // Tuck the window in the bottom right.
  FlingWindow(window.get(), /*left=*/false, /*up=*/false);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));

  // Swipe up on the handle. Test the window is still tucked.
  views::Widget* tuck_handle_widget =
      float_controller->GetTuckHandleWidget(window.get());
  const gfx::Point start(
      tuck_handle_widget->GetWindowBoundsInScreen().CenterPoint());
  GetEventGenerator()->GestureScrollSequence(
      start, start + gfx::Vector2d(0, -8), base::Milliseconds(100),
      /*steps=*/3);
  ASSERT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kUntuckUserAction));

  // Swipe left on the handle. Test that it untucks and magnetizes to the bottom
  // right.
  GetEventGenerator()->GestureScrollSequence(
      start, start + gfx::Vector2d(-8, 0), base::Milliseconds(100),
      /*steps=*/3);
  EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kUntuckUserAction));
}

// Tests that flinging the window straight up or down does not tuck the window.
TEST_F(TabletWindowFloatTest, FlingVertical) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  // The window is magnetized to the bottom right by default.
  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Fling the window straight down. Test that it stays in the bottom right.
  const gfx::Point start =
      GetHeaderView(window.get())->GetBoundsInScreen().CenterPoint();
  GetEventGenerator()->GestureScrollSequence(
      start, start + gfx::Vector2d(0, 10), base::Milliseconds(10), /*steps=*/1);
  auto* float_controller = Shell::Get()->float_controller();
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kBottomRight);
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kTuckUserAction));

  // Fling the window straight up. Test that it moves to the top right.
  GetEventGenerator()->GestureScrollSequence(
      start, start + gfx::Vector2d(0, -10), base::Milliseconds(10),
      /*steps=*/1);
  ASSERT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window.get()));
  CheckMagnetized(window.get(), FloatController::MagnetismCorner::kTopRight);
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kTuckUserAction));
}

// Tests that the tuck education nudge appears when the window is first floated.
TEST_F(TabletWindowFloatTest, BasicTuckNudge) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateAppWindow();

  // Add observer to check that the tuck education nudge was created.
  views::NamedWidgetShownWaiter widget_waiter(
      views::test::AnyWidgetTestPasskey{}, "TuckEducationNudgeWidget");

  // Float window using accelerator.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

  // If waiter never sees the tuck education nudge, it will hang forever, and
  // the test will fail.
  widget_waiter.WaitIfNeededAndGet();

  // Nudge should dismiss properly after animations end.
  EXPECT_TRUE(window->children().empty());

  // TODO(hewer): Add a callback to check that the nudge has properly dismissed
  // after the bounce animations and timer have ended.
}

TEST_F(TabletWindowFloatTest, EducationPreferences) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
  base::SimpleTestClock test_clock;
  TabletModeTuckEducation::SetOverrideClockForTesting(&test_clock);

  // Advance clock so we aren't at zero time.
  test_clock.Advance(base::Hours(25));

  NudgeCounter nudge_counter;

  // Float the nudge three times, count should increment each time.
  for (int i = 0; i < 3; i++) {
    std::unique_ptr<aura::Window> window = CreateAppWindow();

    // Float window using accelerator.
    PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
    ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());

    // Close window and advance time so nudge can be shown again.
    window.reset();
    test_clock.Advance(base::Hours(25));
  }

  EXPECT_EQ(3, nudge_counter.nudge_count());

  // Float the window once more.
  std::unique_ptr<aura::Window> window = CreateAppWindow();
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  window.reset();

  // Counter should not increment as nudge was not shown.
  EXPECT_EQ(3, nudge_counter.nudge_count());

  TabletModeTuckEducation::SetOverrideClockForTesting(nullptr);
}

using TabletWindowFloatSplitviewTest = TabletWindowFloatTest;

// Tests the expected behaviour when a window is floated when there are snapped
// windows on each side.
TEST_F(TabletWindowFloatSplitviewTest, BothSnappedToFloat) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  // Create two windows and snap one on each side.
  auto left_window = CreateAppWindow();
  const WindowSnapWMEvent snap_left(WM_EVENT_SNAP_PRIMARY);
  WindowState::Get(left_window.get())->OnWMEvent(&snap_left);

  auto right_window = CreateAppWindow();
  const WindowSnapWMEvent snap_right(WM_EVENT_SNAP_SECONDARY);
  WindowState::Get(right_window.get())->OnWMEvent(&snap_right);

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  ASSERT_EQ(split_view_controller->state(),
            SplitViewController::State::kBothSnapped);

  // Float the left window. Verify that it is floated, the right window becomes
  // maximized and that we are no longer in splitview.
  wm::ActivateWindow(left_window.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(WindowState::Get(left_window.get())->IsFloated());
  EXPECT_TRUE(WindowState::Get(right_window.get())->IsMaximized());
  EXPECT_FALSE(split_view_controller->InSplitViewMode());
}

// Tests the expected behaviour when a window is floated then snapped.
TEST_F(TabletWindowFloatSplitviewTest, FloatToSnapped) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());

  // If there are no other windows, expect to enter overview. The hotseat will
  // extended and users can pick a second app from there.
  const WindowSnapWMEvent snap_left(WM_EVENT_SNAP_PRIMARY);
  WindowState::Get(window.get())->OnWMEvent(&snap_left);
  ASSERT_TRUE(OverviewController::Get()->InOverviewSession());
  ASSERT_TRUE(split_view_controller->InSplitViewMode());

  // Float the window so we can snap it again. Assert that we are no longer in
  // overview or splitview.
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_FALSE(OverviewController::Get()->InOverviewSession());
  ASSERT_FALSE(split_view_controller->InSplitViewMode());

  // Create a second window.
  auto other_window = CreateAppWindow();
  wm::ActivateWindow(window.get());

  // Tests that when we snap `window` now, `other_window` will get snapped to
  // the opposite side.
  WindowState::Get(window.get())->OnWMEvent(&snap_left);
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
  EXPECT_EQ(split_view_controller->state(),
            SplitViewController::State::kBothSnapped);
  EXPECT_EQ(split_view_controller->primary_window(), window.get());
  EXPECT_EQ(split_view_controller->secondary_window(), other_window.get());

  // Tests that in overview mode, with at least one app window in overview, that
  // we also exit splitview and overview when floating the snapped window.
  ToggleOverview();
  wm::ActivateWindow(window.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_FALSE(OverviewController::Get()->InOverviewSession());
  EXPECT_FALSE(split_view_controller->InSplitViewMode());

  // Tests that when we partial-snap `other_window` now, activating `window`
  // will results in `window` snapped on the opposite side while keeping the
  // partial snap ratio.
  const WindowSnapWMEvent snap_primary_two_third(WM_EVENT_SNAP_PRIMARY,
                                                 chromeos::kTwoThirdSnapRatio);
  WindowState::Get(other_window.get())->OnWMEvent(&snap_primary_two_third);
  ASSERT_TRUE(WindowState::Get(other_window.get())->IsSnapped());

  wm::ActivateWindow(window.get());
  EXPECT_TRUE(WindowState::Get(window.get())->IsSnapped());
  EXPECT_EQ(split_view_controller->state(),
            SplitViewController::State::kBothSnapped);
  EXPECT_EQ(split_view_controller->primary_window(), other_window.get());
  EXPECT_EQ(split_view_controller->secondary_window(), window.get());
  EXPECT_NEAR(chromeos::kOneThirdSnapRatio,
              WindowState::Get(window.get())->snap_ratio().value(),
              /*abs_error=*/0.1);
  EXPECT_NEAR(chromeos::kTwoThirdSnapRatio,
              WindowState::Get(other_window.get())->snap_ratio().value(),
              /*abs_error=*/0.1);
}

// When reset a floated window that's previously snapped, maximize instead of
// snap.
TEST_F(TabletWindowFloatSplitviewTest, ResetFloatToMaximize) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  // Create two windows and snap one on each side.
  std::unique_ptr<aura::Window> window_1 = CreateAppWindow();
  std::unique_ptr<aura::Window> window_2 = CreateAppWindow();

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());

  const WindowSnapWMEvent snap_left(WM_EVENT_SNAP_PRIMARY);
  WindowState::Get(window_1.get())->OnWMEvent(&snap_left);
  const WindowSnapWMEvent snap_right(WM_EVENT_SNAP_SECONDARY);
  WindowState::Get(window_2.get())->OnWMEvent(&snap_right);
  EXPECT_EQ(split_view_controller->state(),
            SplitViewController::State::kBothSnapped);
  EXPECT_EQ(split_view_controller->primary_window(), window_1.get());
  EXPECT_EQ(split_view_controller->secondary_window(), window_2.get());

  // Float `window_1`, `window_2` should be maximized.
  wm::ActivateWindow(window_1.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window_1.get())->IsFloated());
  ASSERT_TRUE(WindowState::Get(window_2.get())->IsMaximized());

  // Float `window_2`, previously floated `window_1` should be maximized instead
  // of restoring back to snapped.
  wm::ActivateWindow(window_2.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window_2.get())->IsFloated());
  ASSERT_TRUE(WindowState::Get(window_1.get())->IsMaximized());
}

// Tests that there is no crash when going from float to always on top in tablet
// mode. Regression test for http://b/317064996.
TEST_F(TabletWindowFloatTest, FloatStateToAlwaysOnTop) {
  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);

  std::unique_ptr<aura::Window> window = CreateFloatedWindow();

  // Make the window always on top. It should exit float state.
  window->SetProperty(aura::client::kZOrderingKey,
                      ui::ZOrderLevel::kFloatingWindow);
  EXPECT_FALSE(WindowState::Get(window.get())->IsFloated());
}

}  // namespace ash