chromium/ash/wm/tablet_mode/tablet_mode_multitask_menu_unittest.cc

// Copyright 2022 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/tablet_mode/tablet_mode_multitask_menu.h"

#include <memory>

#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_cue_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_util.h"
#include "base/command_line.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "chromeos/ui/frame/multitask_menu/multitask_button.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_metrics.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_view.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_view_test_api.h"
#include "chromeos/ui/frame/multitask_menu/split_button_view.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/display/display_switches.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// The vertical distance used to end drag to show and start drag to hide.
constexpr int kMenuDragPoint = 100;

// The vertical position of the multitask menu, in the window (and widget)
// coordinates.
constexpr int kVerticalPosition = 8;

}  // namespace

class TabletModeMultitaskMenuTest : public AshTestBase {
 public:
  TabletModeMultitaskMenuTest() = default;
  TabletModeMultitaskMenuTest(const TabletModeMultitaskMenuTest&) = delete;
  TabletModeMultitaskMenuTest& operator=(const TabletModeMultitaskMenuTest&) =
      delete;
  ~TabletModeMultitaskMenuTest() override = default;

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

    AshTestBase::SetUp();

    TabletModeControllerTestApi().EnterTabletMode();
  }

  void GenerateScroll(int x, int start_y, int end_y) {
    GetEventGenerator()->GestureScrollSequence(
        gfx::Point(x, start_y), gfx::Point(x, end_y), base::Milliseconds(100),
        /*steps=*/3);
  }

  void ShowMultitaskMenu(const aura::Window& window) {
    GenerateScroll(/*x=*/window.bounds().CenterPoint().x(),
                   /*start_y=*/window.bounds().y() + 8,
                   /*end_y=*/window.bounds().y() + kMenuDragPoint);
  }

  void DismissMenu(TabletModeMultitaskMenu* multitask_menu) {
    GetEventGenerator()->GestureTapAt(
        multitask_menu->widget()->GetWindowBoundsInScreen().bottom_center() +
        gfx::Vector2d(0, 10));

    DCHECK(!GetMultitaskMenu());
  }

  void PressHalfButton(const aura::Window* window, bool left) {
    ShowMultitaskMenu(*window);
    auto* multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
    const gfx::Rect half_bounds =
        chromeos::MultitaskMenuViewTestApi(multitask_menu_view)
            .GetHalfButton()
            ->GetBoundsInScreen();
    GetEventGenerator()->GestureTapAt(left ? half_bounds.left_center()
                                           : half_bounds.right_center());
    auto* split_view_controller = SplitViewController::Get(window);
    DCHECK_EQ(split_view_controller->GetPositionOfSnappedWindow(window),
              left ? SnapPosition::kPrimary : SnapPosition::kSecondary);
  }

  void PressPartialPrimary(const aura::Window& window) {
    ShowMultitaskMenu(window);
    DCHECK(GetMultitaskMenu());
    GetEventGenerator()->GestureTapAt(GetMultitaskMenuView(GetMultitaskMenu())
                                          ->partial_button()
                                          ->GetBoundsInScreen()
                                          .left_center());
  }

  void PressPartialSecondary(const aura::Window& window) {
    ShowMultitaskMenu(window);
    DCHECK(GetMultitaskMenu());
    GetEventGenerator()->GestureTapAt(GetMultitaskMenuView(GetMultitaskMenu())
                                          ->partial_button()
                                          ->GetRightBottomButton()
                                          ->GetBoundsInScreen()
                                          .CenterPoint());
  }

  TabletModeMultitaskMenuController* GetMultitaskMenuController() {
    return TabletModeControllerTestApi()
        .tablet_mode_window_manager()
        ->tablet_mode_multitask_menu_controller();
  }

  TabletModeMultitaskMenu* GetMultitaskMenu() {
    return TabletModeControllerTestApi()
        .tablet_mode_window_manager()
        ->tablet_mode_multitask_menu_controller()
        ->multitask_menu();
  }

  chromeos::MultitaskMenuView* GetMultitaskMenuView(
      TabletModeMultitaskMenu* multitask_menu) const {
    return multitask_menu->GetMultitaskMenuViewForTesting();
  }

 protected:
  base::HistogramTester histogram_tester_;
};

// Tests that a scroll down gesture from the top center activates the
// multitask menu.
TEST_F(TabletModeMultitaskMenuTest, BasicShowMenu) {
  auto window = CreateAppWindow();

  ShowMultitaskMenu(*window);

  TabletModeMultitaskMenu* multitask_menu = GetMultitaskMenu();
  ASSERT_TRUE(multitask_menu);
  ASSERT_TRUE(multitask_menu->widget()->GetContentsView()->GetVisible());

  // Tests that a regular window that can be snapped and floated has all
  // buttons.
  chromeos::MultitaskMenuView* multitask_menu_view =
      GetMultitaskMenuView(multitask_menu);
  ASSERT_TRUE(multitask_menu_view);
  chromeos::MultitaskMenuViewTestApi test_api(multitask_menu_view);
  EXPECT_TRUE(test_api.GetHalfButton());
  EXPECT_TRUE(multitask_menu_view->partial_button());
  EXPECT_TRUE(test_api.GetFullButton());
  EXPECT_TRUE(test_api.GetFloatButton());

  // Verify that the menu is horizontally centered.
  EXPECT_EQ(multitask_menu->widget()
                ->GetContentsView()
                ->GetBoundsInScreen()
                .CenterPoint()
                .x(),
            window->GetBoundsInScreen().CenterPoint().x());
}

TEST_F(TabletModeMultitaskMenuTest, SwipeDownTargetArea) {
  auto window = CreateTestWindow(gfx::Rect(800, 600));

  // Scroll down from the top left. Verify no menu.
  GenerateScroll(0, 1, kMenuDragPoint);
  ASSERT_FALSE(GetMultitaskMenu());

  // Scroll down from the top right. Verify no menu.
  GenerateScroll(window->bounds().right(), 1, kMenuDragPoint);
  ASSERT_FALSE(GetMultitaskMenu());

  // Swipe up in the target area. Verify no menu.
  GenerateScroll(window->bounds().CenterPoint().x(), kMenuDragPoint, 8);
  ASSERT_FALSE(GetMultitaskMenu());

  // Start swipe down from the top of the target area.
  GenerateScroll(window->bounds().CenterPoint().x(), 0, kMenuDragPoint);
  ASSERT_TRUE(GetMultitaskMenu());
  DismissMenu(GetMultitaskMenu());

  // Start swipe down from the bottom of the target area.
  // TODO(sophiewen): Replace this with `kHitRegionSize.height()`.
  GenerateScroll(window->bounds().CenterPoint().x(), 15, kMenuDragPoint);
  ASSERT_TRUE(GetMultitaskMenu());
  DismissMenu(GetMultitaskMenu());

  // End swipe down outside the menu.
  GenerateScroll(window->bounds().CenterPoint().x(), 0, 300);
  ASSERT_TRUE(GetMultitaskMenu());

  // Swipe up outside the menu. Verify we close the menu.
  GenerateScroll(window->bounds().CenterPoint().x(), 300, 200);
  ASSERT_FALSE(GetMultitaskMenu());
}

// Tests that a slight touch moved in the menu will trigger a button press.
TEST_F(TabletModeMultitaskMenuTest, PressMoveAndReleaseTouch) {
  auto window = CreateAppWindow(gfx::Rect(800, 600));
  ShowMultitaskMenu(*window);

  // Press and move the touch slightly to mimic a real tap.
  auto* half_button = chromeos::MultitaskMenuViewTestApi(
                          GetMultitaskMenuView(GetMultitaskMenu()))
                          .GetHalfButton();
  GetEventGenerator()->set_current_screen_location(
      half_button->GetBoundsInScreen().left_center());
  GetEventGenerator()->PressMoveAndReleaseTouchBy(0, 3);

  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(window.get())->GetStateType());
}

TEST_F(TabletModeMultitaskMenuTest, SwipeDownInSplitView) {
  auto window1 = CreateTestWindow(gfx::Rect(800, 600));
  PressHalfButton(window1.get(), /*left=*/true);
  auto window2 = CreateTestWindow(gfx::Rect(800, 600));
  PressHalfButton(window2.get(), /*left=*/false);

  // Swipe down on the left window. Test that the menu is shown.
  wm::ActivateWindow(window1.get());
  gfx::Rect left_bounds(window1->bounds());
  GenerateScroll(left_bounds.CenterPoint().x(), 0, 160);
  auto* multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_TRUE(multitask_menu_view);
  ASSERT_TRUE(left_bounds.Contains(multitask_menu_view->GetBoundsInScreen()));

  // Swipe down on the right window. Test that it shows the menu.
  gfx::Rect right_bounds(window2->bounds());
  GenerateScroll(right_bounds.CenterPoint().x(), 0, 150);
  multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_TRUE(multitask_menu_view);
  ASSERT_TRUE(right_bounds.Contains(multitask_menu_view->GetBoundsInScreen()));
}

// Tests no crash when swiping down another window during menu animation.
// http://b/276792842.
TEST_F(TabletModeMultitaskMenuTest, SwipeDownInSplitViewWhileAnimating) {
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Create a larger display so the menu is within the window bounds when split.
  UpdateDisplay("1600x1000");

  auto window1 = CreateTestWindow(gfx::Rect(800, 600));
  PressHalfButton(window1.get(), /*left=*/true);
  auto window2 = CreateTestWindow(gfx::Rect(800, 600));
  PressHalfButton(window2.get(), /*left=*/false);

  // Start swipe down on the left window, then swap to the right window. Swipe
  // distance must be less than the menu height to start an animation.
  gfx::Rect left_bounds(window1->bounds());
  GenerateScroll(left_bounds.CenterPoint().x(), 0,
                 /*end_y=*/kMenuDragPoint);
  gfx::Rect right_bounds(window2->bounds());
  GenerateScroll(right_bounds.CenterPoint().x(), 0,
                 /*end_y=*/kMenuDragPoint);
  EXPECT_TRUE(window2->GetBoundsInScreen().Contains(
      GetMultitaskMenu()->widget()->GetWindowBoundsInScreen()));

  // Test that swipe down both windows at the same time doesn't crash.
  ui::test::EventGenerator* generator = GetEventGenerator();
  generator->PressTouchId(0, gfx::Point(left_bounds.CenterPoint().x(), 0));
  generator->PressTouchId(1, gfx::Point(right_bounds.CenterPoint().x(), 0));
  generator->MoveTouchId(gfx::Point(0, kMenuDragPoint), 0);
  generator->MoveTouchId(gfx::Point(0, kMenuDragPoint), 1);
}

// Tests that the multitask menu cannot be shown while in pinned state.
TEST_F(TabletModeMultitaskMenuTest, SwipeDownInPinnedWindow) {
  // Create and pin a window.
  std::unique_ptr<aura::Window> pinned_window = CreateAppWindow();
  wm::ActivateWindow(pinned_window.get());
  window_util::PinWindow(pinned_window.get(), /*trusted=*/true);

  GenerateScroll(pinned_window->bounds().CenterPoint().x(), 0, 150);
  EXPECT_FALSE(GetMultitaskMenu());
}

// Tests that swipe down outside the menu doesn't crash. Test for b/266742428.
TEST_F(TabletModeMultitaskMenuTest, SwipeDownMenuTwice) {
  auto window = CreateTestWindow(gfx::Rect(800, 600));

  // Scroll down to show the menu.
  GenerateScroll(window->bounds().CenterPoint().x(), 1, kMenuDragPoint);
  ASSERT_TRUE(GetMultitaskMenu());

  // Scroll down outside the menu. Verify that we close the menu.
  GenerateScroll(window->bounds().CenterPoint().x(), 400, 500);
  ASSERT_FALSE(GetMultitaskMenu());

  // Scroll down to show the menu again.
  GenerateScroll(window->bounds().CenterPoint().x(), 1, kMenuDragPoint);
  ASSERT_TRUE(GetMultitaskMenu());

  // Scroll down outside the menu again.
  GenerateScroll(window->bounds().CenterPoint().x(), 400, 500);
  ASSERT_FALSE(GetMultitaskMenu());

  histogram_tester_.ExpectBucketCount(
      chromeos::GetEntryTypeHistogramName(),
      chromeos::MultitaskMenuEntryType::kGestureScroll, 2);
}

// Tests no crash on multiple finger scrolls.
TEST_F(TabletModeMultitaskMenuTest, MultiFingerSroll) {
  auto window = CreateTestWindow();
  const int center_x = window->bounds().CenterPoint().x();

  // Scroll down with 2 fingers.
  const int kTouchPoints = 2;
  gfx::Point points[kTouchPoints] = {
      gfx::Point(center_x, 0),
      gfx::Point(center_x + 10, 0),
  };
  const int kSteps = 15;
  GetEventGenerator()->GestureMultiFingerScroll(kTouchPoints, points, 15,
                                                kSteps, 0, 150);
  EXPECT_TRUE(GetMultitaskMenu());
}

// Tests that a partial drag will show or hide the menu as expected.
TEST_F(TabletModeMultitaskMenuTest, PartialDrag) {
  auto window = CreateTestWindow();
  // Scroll down less than half of the menu height. Tests that the menu does not
  // open.
  GenerateScroll(/*x=*/window->bounds().CenterPoint().x(), /*start_y=*/1,
                 /*end_y=*/20);
  ASSERT_FALSE(GetMultitaskMenu());

  // Scroll down more than half of the menu height. Tests that the menu opens.
  GenerateScroll(/*x=*/window->bounds().CenterPoint().x(), /*start_y=*/1,
                 /*end_y=*/100);
  ASSERT_TRUE(GetMultitaskMenu());
}

// Tests that the menu is closed when the window is closed or destroyed.
TEST_F(TabletModeMultitaskMenuTest, OnWindowDestroying) {
  auto window = CreateTestWindow();

  ShowMultitaskMenu(*window);
  ASSERT_TRUE(GetMultitaskMenu());

  // Close the window.
  window.reset();
  EXPECT_FALSE(GetMultitaskMenu());
}

// Tests that tap outside the menu will close the menu.
TEST_F(TabletModeMultitaskMenuTest, CloseMultitaskMenuOnTap) {
  // Create a display and window that is bigger than the menu.
  UpdateDisplay("1600x1000");
  auto window = CreateAppWindow();

  ShowMultitaskMenu(*window);
  ASSERT_TRUE(GetMultitaskMenu());

  // Tap outside the menu. Verify that we close the menu.
  GetEventGenerator()->GestureTapAt(window->GetBoundsInScreen().CenterPoint());
  EXPECT_FALSE(GetMultitaskMenu());
}

// Tests that pressing a button before the show animation ends closes the menu
// (http://b/279355302).
TEST_F(TabletModeMultitaskMenuTest, CloseMultitaskMenuOnButtonPress) {
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Swipe down the menu partially to start an animation.
  auto window = CreateAppWindow();
  GenerateScroll(window->bounds().CenterPoint().x(), 0,
                 /*end_y=*/60);
  GestureTapOn(chromeos::MultitaskMenuViewTestApi(
                   GetMultitaskMenuView(GetMultitaskMenu()))
                   .GetFloatButton());

  // Wait for the TabletModeMultitaskMenuView layer to fade out.
  ui::LayerAnimationStoppedWaiter().Wait(
      GetMultitaskMenu()->widget()->GetContentsView()->layer());

  ASSERT_FALSE(GetMultitaskMenu());
}

TEST_F(TabletModeMultitaskMenuTest, CloseOnDoubleTapDivider) {
  auto window1 = CreateTestWindow(gfx::Rect(800, 600));
  auto window2 = CreateTestWindow(gfx::Rect(800, 600));

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  split_view_controller->SnapWindow(window1.get(), SnapPosition::kPrimary);
  split_view_controller->SnapWindow(window2.get(), SnapPosition::kSecondary);

  // Open the menu on one of the windows.
  ShowMultitaskMenu(*window1);
  ASSERT_TRUE(GetMultitaskMenu());

  // Double tap on the divider center.
  const gfx::Point divider_center =
      split_view_controller->split_view_divider()
          ->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint();
  GetEventGenerator()->GestureTapAt(divider_center);
  GetEventGenerator()->GestureTapAt(divider_center);
  ASSERT_FALSE(GetMultitaskMenu());
}

TEST_F(TabletModeMultitaskMenuTest, HideMultitaskMenuInOverview) {
  auto window = CreateTestWindow();

  ShowMultitaskMenu(*window);

  auto* event_handler = TabletModeControllerTestApi()
                            .tablet_mode_window_manager()
                            ->tablet_mode_multitask_menu_controller();
  auto* multitask_menu = event_handler->multitask_menu();
  ASSERT_TRUE(multitask_menu);
  ASSERT_TRUE(multitask_menu->widget()->GetContentsView()->GetVisible());

  EnterOverview();

  // Verify that the menu is hidden.
  EXPECT_FALSE(GetMultitaskMenu());
}

// Tests that the multitask menu gets updated after a button is pressed.
TEST_F(TabletModeMultitaskMenuTest, HalfButtonFunctionality) {
  // Create a test window wide enough to show the menu even after it is split in
  // half.
  UpdateDisplay("1600x1000");
  auto window = CreateTestWindow();
  ShowMultitaskMenu(*window);

  // Press the primary half split button.
  auto* half_button = chromeos::MultitaskMenuViewTestApi(
                          GetMultitaskMenuView(GetMultitaskMenu()))
                          .GetHalfButton();
  GetEventGenerator()->GestureTapAt(
      half_button->GetBoundsInScreen().left_center());
  histogram_tester_.ExpectBucketCount(
      chromeos::GetActionTypeHistogramName(),
      chromeos::MultitaskMenuActionType::kHalfSplitButton, 1);

  // Verify that the window has been snapped in half.
  ASSERT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(window.get())->GetStateType());
  const gfx::Rect work_area_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
  EXPECT_EQ(work_area_bounds.width() * 0.5f,
            window->GetBoundsInScreen().width() +
                kSplitviewDividerShortSideLength / 2);

  // Verify that the multitask menu has been closed.
  ASSERT_FALSE(GetMultitaskMenu());

  // Verify that the multitask menu is centered on the new window size.
  ShowMultitaskMenu(*window);
  auto* multitask_menu = GetMultitaskMenu();
  ASSERT_TRUE(multitask_menu);
  EXPECT_EQ(window->GetBoundsInScreen().CenterPoint().x(),
            GetMultitaskMenuView(multitask_menu)
                ->GetBoundsInScreen()
                .CenterPoint()
                .x());
}

TEST_F(TabletModeMultitaskMenuTest, PartialButtonFunctionality) {
  auto window = CreateTestWindow();

  // Test that primary button snaps to 0.67f screen ratio.
  PressPartialPrimary(*window);
  ASSERT_EQ(chromeos::WindowStateType::kPrimarySnapped,
            WindowState::Get(window.get())->GetStateType());
  const gfx::Rect work_area_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
  const int divider_delta = kSplitviewDividerShortSideLength / 2;
  EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kTwoThirdSnapRatio),
            window->bounds().width() + divider_delta);
  ASSERT_FALSE(GetMultitaskMenu());
  histogram_tester_.ExpectBucketCount(
      chromeos::GetActionTypeHistogramName(),
      chromeos::MultitaskMenuActionType::kPartialSplitButton, 1);

  // Test that secondary button snaps to 0.33f screen ratio.
  PressPartialSecondary(*window);
  ASSERT_EQ(chromeos::WindowStateType::kSecondarySnapped,
            WindowState::Get(window.get())->GetStateType());
  EXPECT_EQ(std::round(work_area_bounds.width() * chromeos::kOneThirdSnapRatio),
            window->bounds().width() + divider_delta);
  ASSERT_FALSE(GetMultitaskMenu());
  histogram_tester_.ExpectBucketCount(
      chromeos::GetActionTypeHistogramName(),
      chromeos::MultitaskMenuActionType::kPartialSplitButton, 2);
}

// Tests that the menu bounds are adjusted if the window is narrower than the
// menu for two partial split windows.
TEST_F(TabletModeMultitaskMenuTest, AdjustedMenuBounds) {
  auto window1 = CreateTestWindow();
  PressPartialPrimary(*window1);
  auto window2 = CreateTestWindow();
  PressPartialSecondary(*window2);

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(split_view_controller->IsWindowInSplitView(window1.get()));
  ASSERT_TRUE(split_view_controller->IsWindowInSplitView(window2.get()));

  // Test that the menu fits on the 1/3 window on the right.
  const gfx::Rect work_area =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
  EXPECT_EQ(std::round(work_area.width() * chromeos::kOneThirdSnapRatio),
            window2->bounds().width() + kSplitviewDividerShortSideLength / 2);
  ShowMultitaskMenu(*window2);
  ASSERT_TRUE(GetMultitaskMenu());
  EXPECT_EQ(work_area.right(),
            GetMultitaskMenu()->widget()->GetWindowBoundsInScreen().right());

  // Swap windows so the 1/3 window is on the left. Test that the menu fits.
  split_view_controller->SwapWindows();
  ShowMultitaskMenu(*window2);
  EXPECT_EQ(work_area.x(),
            GetMultitaskMenu()->widget()->GetWindowBoundsInScreen().x());
}

// Tests that the split buttons are enabled/disabled based on min sizes.
TEST_F(TabletModeMultitaskMenuTest, WindowMinimumSizes) {
  UpdateDisplay("800x600");
  aura::test::TestWindowDelegate delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &delegate, /*id=*/-1, gfx::Rect(800, 600)));
  wm::ActivateWindow(window.get());
  EXPECT_TRUE(WindowState::Get(window.get())->CanMaximize());

  const gfx::Rect work_area_bounds =
      display::Screen::GetScreen()->GetPrimaryDisplay().work_area();

  // Set the min width to 0.4 of the work area. Since 1/3 < minWidth <= 1/2,
  // only the 1/3 option is disabled.
  delegate.set_minimum_size(
      gfx::Size(work_area_bounds.width() * 0.4f, work_area_bounds.height()));
  ShowMultitaskMenu(*window);
  ASSERT_TRUE(GetMultitaskMenu());
  chromeos::MultitaskMenuView* multitask_menu_view =
      GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_TRUE(
      chromeos::MultitaskMenuViewTestApi(multitask_menu_view).GetHalfButton());
  ASSERT_TRUE(multitask_menu_view->partial_button()->GetEnabled());
  ASSERT_FALSE(multitask_menu_view->partial_button()
                   ->GetRightBottomButton()
                   ->GetEnabled());
  GetMultitaskMenu()->Reset();

  // Set the min width to 0.6 of the work area. Since 1/2 < minWidth <= 2/3, the
  // half button is hidden and only the 2/3 option is enabled.
  delegate.set_minimum_size(
      gfx::Size(work_area_bounds.width() * 0.6f, work_area_bounds.height()));
  ShowMultitaskMenu(*window);
  multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_FALSE(
      chromeos::MultitaskMenuViewTestApi(multitask_menu_view).GetHalfButton());
  EXPECT_TRUE(multitask_menu_view->partial_button()->GetEnabled());
  ASSERT_FALSE(multitask_menu_view->partial_button()
                   ->GetRightBottomButton()
                   ->GetEnabled());
  GetMultitaskMenu()->Reset();

  // Set the min width to 0.7 of the work area. Since minWidth > 2/3, both the
  // split buttons are hidden.
  delegate.set_minimum_size(
      gfx::Size(work_area_bounds.width() * 0.7f, work_area_bounds.height()));
  ShowMultitaskMenu(*window);
  multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  EXPECT_FALSE(
      chromeos::MultitaskMenuViewTestApi(multitask_menu_view).GetHalfButton());
  EXPECT_FALSE(multitask_menu_view->partial_button());
  GetMultitaskMenu()->Reset();

  // Snap `window` to 1/3 to set its snap ratio to 1/3.
  const WindowSnapWMEvent snap_left(WM_EVENT_SNAP_PRIMARY,
                                    chromeos::kOneThirdSnapRatio);
  WindowState::Get(window.get())->OnWMEvent(&snap_left);
  const WMEvent restore(WM_EVENT_RESTORE);
  WindowState::Get(window.get())->OnWMEvent(&restore);

  // Set minimum size to make `window` snappable in 1/2 ratio but not in 1/3
  // ratio.
  delegate.set_minimum_size(gfx::Size(work_area_bounds.width() * 0.4, 0));

  // Half button should be visible according to snappability with the default
  // snap ratio instead of the window's current snap ratio.
  ShowMultitaskMenu(*window);
  multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  EXPECT_TRUE(
      chromeos::MultitaskMenuViewTestApi(multitask_menu_view).GetHalfButton());
  EXPECT_TRUE(multitask_menu_view->partial_button()->GetEnabled());
  ASSERT_FALSE(multitask_menu_view->partial_button()
                   ->GetRightBottomButton()
                   ->GetEnabled());
}

// Tests that if a window cannot be snapped or floated, the buttons will not
// be shown.
TEST_F(TabletModeMultitaskMenuTest, HiddenButtons) {
  UpdateDisplay("800x600");

  // A window with a minimum size of 600x600 will not be snappable or
  // floatable.
  aura::test::TestWindowDelegate window_delegate;
  std::unique_ptr<aura::Window> window(CreateTestWindowInShellWithDelegate(
      &window_delegate, /*id=*/-1, gfx::Rect(700, 700)));
  window_delegate.set_minimum_size(gfx::Size(600, 600));
  wm::ActivateWindow(window.get());

  ShowMultitaskMenu(*window);

  // Tests that only one button, the fullscreen button shows up.
  TabletModeMultitaskMenu* multitask_menu = GetMultitaskMenu();
  ASSERT_TRUE(multitask_menu);
  chromeos::MultitaskMenuView* multitask_menu_view =
      GetMultitaskMenuView(multitask_menu);
  ASSERT_TRUE(multitask_menu_view);
  chromeos::MultitaskMenuViewTestApi test_api(multitask_menu_view);
  EXPECT_FALSE(test_api.GetHalfButton());
  EXPECT_FALSE(multitask_menu_view->partial_button());
  EXPECT_TRUE(test_api.GetFullButton());
  EXPECT_FALSE(test_api.GetFloatButton());
}

// Tests that the cue is still showing when the menu is opened, and it has been
// transformed to the correct position below the menu.
TEST_F(TabletModeMultitaskMenuTest, CueTransformOnShowMenu) {
  auto window = CreateAppWindow();

  auto* multitask_cue_controller =
      GetMultitaskMenuController()->multitask_cue_controller();
  ASSERT_TRUE(multitask_cue_controller);
  EXPECT_TRUE(multitask_cue_controller->cue_layer());

  // Cue should still be showing when the menu is activated.
  ShowMultitaskMenu(*window);
  ASSERT_TRUE(multitask_cue_controller);
  ui::Layer* cue_layer = multitask_cue_controller->cue_layer();
  EXPECT_TRUE(cue_layer);

  // Verify cue is transformed to the right position.
  const gfx::Rect expected_bounds(
      (window->bounds().width() - TabletModeMultitaskCueController::kCueWidth) /
          2,
      GetMultitaskMenuView(GetMultitaskMenu())->bounds().bottom() +
          kVerticalPosition + TabletModeMultitaskCueController::kCueYOffset,
      TabletModeMultitaskCueController::kCueWidth,
      TabletModeMultitaskCueController::kCueHeight);
  EXPECT_EQ(expected_bounds, cue_layer->GetTargetTransform().MapRect(
                                 cue_layer->GetTargetBounds()));

  // Cue should still dismiss via timer after menu opened.
  multitask_cue_controller->FireCueDismissTimerForTesting();

  ASSERT_TRUE(multitask_cue_controller);
  EXPECT_FALSE(multitask_cue_controller->cue_layer());
}

// Tests that the cue appears on the correct window when the multitask menu is
// activated on different windows in split view.
TEST_F(TabletModeMultitaskMenuTest, CueCorrectWindowInSplitView) {
  auto window1 = CreateAppWindow();
  PressPartialPrimary(*window1);
  auto window2 = CreateAppWindow();
  PressPartialSecondary(*window2);

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(split_view_controller->IsWindowInSplitView(window1.get()));
  ASSERT_TRUE(split_view_controller->IsWindowInSplitView(window2.get()));

  // Show the menu and cue on the first window.
  ShowMultitaskMenu(*window1);
  auto* multitask_cue_controller =
      GetMultitaskMenuController()->multitask_cue_controller();
  ASSERT_TRUE(multitask_cue_controller);
  EXPECT_TRUE(multitask_cue_controller->cue_layer());
  EXPECT_EQ(window1.get(), multitask_cue_controller->window_);

  // Show the menu and cue on the second window.
  ShowMultitaskMenu(*window2);
  multitask_cue_controller =
      GetMultitaskMenuController()->multitask_cue_controller();
  ASSERT_TRUE(multitask_cue_controller);
  EXPECT_TRUE(multitask_cue_controller->cue_layer());
  EXPECT_EQ(window2.get(), multitask_cue_controller->window_);
}

// Tests that the bottom window can open the multitask menu in portrait mode. In
// portrait primary view, the bottom window is on the right.
// ----------------------
// |   Top   |  Bottom  |
// ----------------------
TEST_F(TabletModeMultitaskMenuTest, ShowBottomMenuPortraitPrimary) {
  ScreenOrientationControllerTestApi test_api(
      Shell::Get()->screen_orientation_controller());
  test_api.SetDisplayRotation(display::Display::ROTATE_270,
                              display::Display::RotationSource::ACTIVE);
  ASSERT_EQ(chromeos::OrientationType::kPortraitPrimary,
            test_api.GetCurrentOrientation());

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  std::unique_ptr<aura::Window> top_window(CreateAppWindow());
  std::unique_ptr<aura::Window> bottom_window(CreateAppWindow());
  split_view_controller->SnapWindow(top_window.get(), SnapPosition::kPrimary);
  split_view_controller->SnapWindow(bottom_window.get(),
                                    SnapPosition::kSecondary);
  EXPECT_FALSE(
      IsPhysicallyLeftOrTop(SnapPosition::kSecondary, bottom_window.get()));
  wm::ActivateWindow(bottom_window.get());

  // Event generation coordinates are relative to the natural origin, but
  // `window` bounds are relative to the portrait origin. Scroll from the
  // divider toward the right to open the menu.
  const gfx::Rect bounds(bottom_window->GetBoundsInScreen());
  const gfx::Point start(bounds.y() + 8, bounds.CenterPoint().x());
  const gfx::Point end(bounds.y() + kMenuDragPoint, bounds.CenterPoint().x());
  GetEventGenerator()->GestureScrollSequence(start, end,
                                             base::Milliseconds(100),
                                             /*steps=*/3);
  auto* multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_TRUE(multitask_menu_view);
  ASSERT_TRUE(bounds.Contains(multitask_menu_view->GetBoundsInScreen()));
}

// Tests that the bottom window can open the multitask menu in portrait mode. In
// portrait secondary view, the bottom window is on the left.
// ----------------------
// |  Bottom  |   Top   |
// ----------------------
// TODO(b/270175923): Temporarily disabled for decreased target area.
TEST_F(TabletModeMultitaskMenuTest, DISABLED_ShowBottomMenuPortraitSecondary) {
  ScreenOrientationControllerTestApi test_api(
      Shell::Get()->screen_orientation_controller());
  test_api.SetDisplayRotation(display::Display::ROTATE_90,
                              display::Display::RotationSource::ACTIVE);
  ASSERT_EQ(chromeos::OrientationType::kPortraitSecondary,
            test_api.GetCurrentOrientation());

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  std::unique_ptr<aura::Window> bottom_window(CreateAppWindow());
  std::unique_ptr<aura::Window> top_window(CreateAppWindow());
  split_view_controller->SnapWindow(bottom_window.get(),
                                    SnapPosition::kPrimary);
  split_view_controller->SnapWindow(top_window.get(), SnapPosition::kSecondary);
  EXPECT_FALSE(
      IsPhysicallyLeftOrTop(SnapPosition::kPrimary, bottom_window.get()));
  wm::ActivateWindow(bottom_window.get());

  // Event generation coordinates are relative to the natural origin, but
  // `window` bounds are relative to the portrait origin. Scroll from the
  // divider toward the left to open the menu.
  const gfx::Rect bounds(bottom_window->bounds());
  const gfx::Point start(bounds.y() + 8, bounds.CenterPoint().x());
  const gfx::Point end(bounds.y() - kMenuDragPoint, bounds.CenterPoint().x());
  GetEventGenerator()->GestureScrollSequence(start, end,
                                             base::Milliseconds(100),
                                             /*steps=*/3);
  auto* multitask_menu_view = GetMultitaskMenuView(GetMultitaskMenu());
  ASSERT_TRUE(multitask_menu_view);
  ASSERT_TRUE(bounds.Contains(multitask_menu_view->GetBoundsInScreen()));
}

// Tests that when we exit tablet mode with the multitask menu open, there is no
// crash. Regression test for b/273835755.
TEST_F(TabletModeMultitaskMenuTest, NoCrashWhenExitingTabletMode) {
  // We need to use a non zero duration otherwise the fade out animation will
  // complete immediately and destroy the multitask menu before the tablet mode
  // window manager gets destroyed, which is not what happens on a real device.
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  auto window = CreateAppWindow();
  ShowMultitaskMenu(*window);
  TabletModeControllerTestApi().LeaveTabletMode();
}

// Tests that update drag does not cause a crash. Test for http://b/290102602.
TEST_F(TabletModeMultitaskMenuTest, NoCrashDuringUpdateDrag) {
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  auto window = CreateAppWindow();

  // Partially drag down to start an animation. `end_y` must be less than half
  // the menu height to animate toward close.
  GenerateScroll(/*x=*/window->bounds().CenterPoint().x(), /*start_y=*/1,
                 /*end_y=*/50);
  auto* multitask_menu = GetMultitaskMenu();
  auto* menu_layer = multitask_menu->widget()->GetContentsView()->layer();
  ASSERT_TRUE(multitask_menu && menu_layer);
  ui::LayerAnimator* animator = menu_layer->GetAnimator();
  EXPECT_TRUE(animator->is_animating());
  // When animating to hide, the menu will be translated up by `translation_y`,
  // equal to the y translation in `initial_transform` in the constructor.
  const int translation_y =
      multitask_menu->widget()->GetContentsView()->height() + kVerticalPosition;
  auto transform = gfx::Transform::MakeTranslation(0, -translation_y);
  EXPECT_EQ(transform, menu_layer->GetTargetTransform());

  // Start another drag to abort the current animation.
  const int current_y = 10;
  multitask_menu->UpdateDrag(current_y, /*down=*/false);
  ASSERT_TRUE(multitask_menu && menu_layer);
  EXPECT_FALSE(animator->is_animating());
  const int initial_y =
      multitask_menu->widget()->GetContentsView()->bounds().bottom();
  transform = gfx::Transform::MakeTranslation(0, current_y - initial_y);
  EXPECT_EQ(transform, menu_layer->transform());

  // If we start a drag in a different direction, ensure we continue to set
  // transform.
  multitask_menu->UpdateDrag(current_y, /*down=*/true);
  ASSERT_TRUE(multitask_menu && menu_layer);
  EXPECT_FALSE(animator->is_animating());
  // transform = gfx::Transform::MakeTranslation(0, y - initial_y);
  EXPECT_EQ(transform, menu_layer->transform());
}

// Test that the window is created on the target window. This can crash if
// EventType::kScrollFlingStart is sent quickly enough after
// EventType::kGestureScrollUpdate, causing the controller to create the menu on
// the split view divider (b/293954921).
TEST_F(TabletModeMultitaskMenuTest, NoCrashWhenDraggingSplitViewDivider) {
  ui::ScopedAnimationDurationScaleMode test_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
  UpdateDisplay("1600x1000");
  auto window = CreateAppWindow();
  PressPartialPrimary(*window);

  // Start dragging on the menu.
  const gfx::Point center_point(window->bounds().CenterPoint().x(), 0);
  auto* event_generator = GetEventGenerator();
  event_generator->PressTouchId(/*touch_id=*/0, center_point);
  event_generator->MoveTouchIdBy(/*touch_id=*/0, 0, 100);
  ASSERT_TRUE(GetMultitaskMenu());

  // Without releasing the first finger, start a fling on the divider and close
  // the menu (this can happen when it loses focus from the second touch).
  GetMultitaskMenu()->Reset();
  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  auto* split_view_divider = split_view_controller->split_view_divider();
  const gfx::Point divider_center =
      split_view_divider->GetDividerBoundsInScreen(/*is_dragging=*/false)
          .CenterPoint();
  event_generator->PressTouchId(/*touch_id=*/1, divider_center);
  event_generator->MoveTouchIdBy(/*touch_id=*/1, -10, 0);
  event_generator->ReleaseTouchId(/*touch_id=*/1);

  // Test that, even though the target window is the divider, we don't try to
  // create the menu on the split view divider.
  CHECK_EQ(GetMultitaskMenuController()->target_window_for_test(),
           split_view_divider->GetDividerWindow());
  EXPECT_FALSE(GetMultitaskMenu());
}

TEST_F(TabletModeMultitaskMenuTest, HidesWhenMinimized) {
  auto window = CreateAppWindow();
  ShowMultitaskMenu(*window);

  Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
      AcceleratorAction::kWindowMinimize, {});
  ASSERT_TRUE(WindowState::Get(window.get())->IsMinimized());
  EXPECT_FALSE(GetMultitaskMenu());
}

TEST_F(TabletModeMultitaskMenuTest, NoTabletModeWindowManagerInKiosk) {
  SimulateKioskMode(user_manager::UserType::kKioskApp);
  auto window = CreateAppWindow(gfx::Rect(800, 600));

  // In Kiosk session there is no `TabletModeWindowManager` since the UI tablet
  // mode is blocked.
  EXPECT_EQ(TabletModeControllerTestApi().tablet_mode_window_manager(),
            nullptr);
}

namespace {

class TestHandler : public ui::EventHandler {
 public:
  TestHandler() = default;
  TestHandler(const TestHandler&) = delete;
  TestHandler& operator=(const TestHandler&) = delete;
  ~TestHandler() override = default;

  // ui::EventHandler:
  void OnTouchEvent(ui::TouchEvent* event) override { flags_ = event->flags(); }
  int GetFlagsAndReset() {
    EXPECT_NE(-1, flags_);
    int ret = flags_;
    flags_ = -1;
    return ret;
  }

  int flags() const { return flags_; }

 private:
  // latest flags.
  int flags_ = -1;
};

}  // namespace

// Make sure that swipe down will set the ui::EF_RESERVED_FOR_GESTURE flag
// on touch event, while swipe right will not.
TEST_F(TabletModeMultitaskMenuTest, BlockSwipeDown) {
  auto* menu_controller = TabletModeControllerTestApi()
                              .tablet_mode_window_manager()
                              ->tablet_mode_multitask_menu_controller();

  auto* root = Shell::GetPrimaryRootWindow();
  TestHandler test_handler;
  root->AddPreTargetHandler(&test_handler);

  auto window = CreateAppWindow();
  auto* generator = GetEventGenerator();

  // Start slightly off the edge.
  const gfx::Point starting_point(
      display::Screen::GetScreen()->GetPrimaryDisplay().bounds().width() / 2,
      3);
  {
    // Emulate swipe down by touches.

    gfx::Point touch_point(starting_point);
    generator->PressTouch(touch_point);
    // Trigger Scroll Begin
    touch_point.set_y(5);
    generator->MoveTouch(touch_point);
    EXPECT_FALSE(menu_controller->is_drag_active_for_test());
    EXPECT_FALSE(menu_controller->reserved_for_gesture_sent_for_test());
    EXPECT_FALSE(test_handler.GetFlagsAndReset() & ui::EF_RESERVED_FOR_GESTURE);

    // Trigger Scroll Update which sets `is_drag_active` to true.
    touch_point.set_y(10);
    generator->MoveTouch(touch_point);
    EXPECT_TRUE(menu_controller->is_drag_active_for_test());
    EXPECT_FALSE(menu_controller->reserved_for_gesture_sent_for_test());
    EXPECT_FALSE(test_handler.GetFlagsAndReset() & ui::EF_RESERVED_FOR_GESTURE);

    // Next touch event should have the EF_RESERVED_FOR_GESTURE flag.
    touch_point.set_y(15);
    generator->MoveTouch(touch_point);
    EXPECT_TRUE(menu_controller->reserved_for_gesture_sent_for_test());
    EXPECT_TRUE(test_handler.GetFlagsAndReset() & ui::EF_RESERVED_FOR_GESTURE);

    // After this, touches should be blocked.
    touch_point.set_y(16);
    generator->MoveTouch(touch_point);
    EXPECT_EQ(-1, test_handler.flags());

    generator->ReleaseTouch();

    // The controller should be in clean state.
    EXPECT_FALSE(menu_controller->is_drag_active_for_test());
    EXPECT_FALSE(menu_controller->reserved_for_gesture_sent_for_test());
    EXPECT_EQ(-1, test_handler.flags());
  }

  // Emulate swipe right by touches. It should never trigger drag nor
  // set reserved_for_gesture_sent_for_test().
  {
    gfx::Point touch_point(starting_point);
    generator->PressTouch(touch_point);

    for (int i = 0; i < 3; i++) {
      touch_point.set_x(touch_point.x() + 5);
      generator->MoveTouch(touch_point);
      EXPECT_FALSE(menu_controller->is_drag_active_for_test());
      EXPECT_FALSE(menu_controller->reserved_for_gesture_sent_for_test());
      EXPECT_FALSE(test_handler.GetFlagsAndReset() &
                   ui::EF_RESERVED_FOR_GESTURE);
    }
    generator->ReleaseTouch();
  }

  // Cleanup
  root->RemovePreTargetHandler(&test_handler);
}

}  // namespace ash