chromium/ash/wm_mode/wm_mode_unittests.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 <string>

#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm_mode/pie_menu_view.h"
#include "ash/wm_mode/wm_mode_button_tray.h"
#include "ash/wm_mode/wm_mode_controller.h"
#include "base/containers/contains.h"
#include "base/test/scoped_feature_list.h"
#include "ui/base/models/image_model.h"
#include "ui/compositor/layer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/image_view.h"

namespace ash {

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

  static WmModeButtonTray* GetWmModeButtonTrayForRoot(aura::Window* root) {
    auto* root_controller = RootWindowController::ForWindow(root);
    DCHECK(root_controller);
    return root_controller->GetStatusAreaWidget()->wm_mode_button_tray();
  }

  static bool IsRootWindowDimmed(aura::Window* root) {
    return WmModeController::Get()->dimmers_.contains(root);
  }

  static views::View* GetPieMenuButtonById(int button_id) {
    auto* controller = WmModeController::Get();
    CHECK(controller->pie_menu_view_);
    return controller->pie_menu_view_->GetButtonByIdAsView(button_id);
  }

  static PieMenuView* GetPieMenuView() {
    return WmModeController::Get()->pie_menu_view_;
  }

  static PieSubMenuContainerView* GetPieSubMenuContainerView(int button_id) {
    return GetPieMenuView()->GetOrAddSubMenuForButton(button_id);
  }

  // AshTestBase:
  void SetUp() override {
    scoped_feature_list_.InitAndEnableFeature(features::kWmMode);
    AshTestBase::SetUp();
  }

  void LeftClickPieMenuButton(int button_id) {
    auto* event_generator = GetEventGenerator();
    const auto button_center =
        GetPieMenuView()->GetButtonContentsCenterInScreen(button_id);
    event_generator->MoveMouseTo(button_center);
    event_generator->ClickLeftButton();
  }

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

TEST_F(WmModeTests, Basic) {
  auto* controller = WmModeController::Get();
  EXPECT_FALSE(controller->is_active());
  EXPECT_FALSE(controller->layer());
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());
  EXPECT_TRUE(controller->layer());
  controller->Toggle();
  EXPECT_FALSE(controller->is_active());
  EXPECT_FALSE(controller->layer());
}

TEST_F(WmModeTests, ToggleFromTray) {
  auto* controller = WmModeController::Get();
  EXPECT_FALSE(controller->is_active());

  WmModeButtonTray* tray_button =
      GetWmModeButtonTrayForRoot(Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(tray_button);
  LeftClickOn(tray_button);
  EXPECT_TRUE(controller->is_active());

  LeftClickOn(tray_button);
  EXPECT_FALSE(controller->is_active());
}

TEST_F(WmModeTests, TraysOnMultipleDisplays) {
  display_manager()->AddRemoveDisplay();
  auto roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 2u);

  auto* controller = WmModeController::Get();
  EXPECT_FALSE(controller->is_active());

  WmModeButtonTray* tray_button_1 = GetWmModeButtonTrayForRoot(roots[0]);
  WmModeButtonTray* tray_button_2 = GetWmModeButtonTrayForRoot(roots[1]);
  ASSERT_TRUE(tray_button_1);
  ASSERT_TRUE(tray_button_2);

  LeftClickOn(tray_button_2);
  EXPECT_TRUE(controller->is_active());
  EXPECT_TRUE(tray_button_1->is_active());
  EXPECT_TRUE(tray_button_2->is_active());

  // Returns true if `image_view` has the same `vector_icon` on its image model.
  auto has_same_vector_icon = [](const views::ImageView* image_view,
                                 const gfx::VectorIcon& vector_icon) -> bool {
    const auto image_model = image_view->GetImageModel();
    if (!image_model.IsVectorIcon() || image_model.GetVectorIcon().is_empty())
      return false;
    return std::string(vector_icon.name) ==
           std::string(image_model.GetVectorIcon().vector_icon()->name);
  };

  EXPECT_TRUE(has_same_vector_icon(tray_button_1->GetImageViewForTesting(),
                                   kWmModeOnIcon));
  EXPECT_TRUE(has_same_vector_icon(tray_button_2->GetImageViewForTesting(),
                                   kWmModeOnIcon));

  LeftClickOn(tray_button_1);
  EXPECT_FALSE(controller->is_active());
  EXPECT_FALSE(tray_button_1->is_active());
  EXPECT_FALSE(tray_button_2->is_active());

  EXPECT_TRUE(has_same_vector_icon(tray_button_1->GetImageViewForTesting(),
                                   kWmModeOffIcon));
  EXPECT_TRUE(has_same_vector_icon(tray_button_2->GetImageViewForTesting(),
                                   kWmModeOffIcon));
}

TEST_F(WmModeTests, ScreenDimming) {
  auto* controller = WmModeController::Get();
  EXPECT_FALSE(controller->is_active());
  auto roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 1u);
  EXPECT_FALSE(IsRootWindowDimmed(roots[0]));

  controller->Toggle();
  EXPECT_TRUE(IsRootWindowDimmed(roots[0]));
  EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer()));

  // Add a new display while the mode is active, and expect that it gets dimmed.
  display_manager()->AddRemoveDisplay();
  roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 2u);
  EXPECT_TRUE(IsRootWindowDimmed(roots[0]));
  EXPECT_TRUE(IsRootWindowDimmed(roots[1]));

  // Deactivate the mode, and all displays are back to normal.
  controller->Toggle();
  EXPECT_FALSE(IsRootWindowDimmed(roots[0]));
  EXPECT_FALSE(IsRootWindowDimmed(roots[1]));
}

TEST_F(WmModeTests, WindowSelection) {
  // Create 2 displays with one window on each.
  UpdateDisplay("800x700,801+0-800x700");
  auto roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 2u);
  auto win1 = CreateAppWindow(gfx::Rect(50, 60, 400, 400));
  auto win2 = CreateAppWindow(gfx::Rect(1000, 200, 400, 400));

  auto* controller = WmModeController::Get();
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());

  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseToCenterOf(win2.get());
  event_generator->ClickLeftButton();
  EXPECT_EQ(controller->selected_window(), win2.get());
  EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer()));

  // Clicking outside the bounds of any window will remove any window
  // selection. However, the layer remains parented to the same root window.
  event_generator->MoveMouseTo(win2->GetBoundsInScreen().bottom_right() +
                               gfx::Vector2d(20, 20));
  event_generator->ClickLeftButton();
  EXPECT_FALSE(controller->selected_window());
  EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer()));

  // The layer will change roots once cursor moves to its display, and clicked
  // even if there is no selected window.
  event_generator->MoveMouseTo(gfx::Point(0, 0));
  event_generator->ClickLeftButton();
  EXPECT_FALSE(controller->selected_window());
  EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer()));

  event_generator->MoveMouseToCenterOf(win1.get());
  event_generator->ClickLeftButton();
  EXPECT_EQ(controller->selected_window(), win1.get());
}

TEST_F(WmModeTests, RemovingSelectedRoot) {
  display_manager()->AddRemoveDisplay();
  auto roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 2u);
  auto* controller = WmModeController::Get();
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());

  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseTo(roots[1]->GetBoundsInScreen().CenterPoint());
  event_generator->ClickLeftButton();
  EXPECT_TRUE(roots[1]->layer()->Contains(controller->layer()));

  // Remove the second display (which is currently selected), and expect that
  // the controller's layer will move to the primary display's root layer.
  display_manager()->AddRemoveDisplay();
  roots = Shell::GetAllRootWindows();
  EXPECT_EQ(roots.size(), 1u);
  EXPECT_TRUE(roots[0]->layer()->Contains(controller->layer()));

  controller->Toggle();
  EXPECT_FALSE(controller->is_active());
}

TEST_F(WmModeTests, PieMenuVisibility) {
  UpdateDisplay("800x700");
  auto roots = Shell::GetAllRootWindows();
  auto win1 = CreateAppWindow(gfx::Rect(400, 400));
  auto win2 = CreateAppWindow(gfx::Rect(400, 300, 400, 400));

  auto* controller = WmModeController::Get();
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());
  // The pie menu should be created but not visible.
  ASSERT_TRUE(controller->pie_menu_widget());
  EXPECT_FALSE(controller->pie_menu_widget()->IsVisible());

  auto* event_generator = GetEventGenerator();

  for (auto* window : {win1.get(), win2.get()}) {
    event_generator->MoveMouseToCenterOf(window);
    event_generator->ClickLeftButton();
    EXPECT_EQ(controller->selected_window(), window);
    EXPECT_TRUE(controller->pie_menu_widget()->IsVisible());
    EXPECT_EQ(
        controller->pie_menu_widget()->GetWindowBoundsInScreen().CenterPoint(),
        window->GetBoundsInScreen().CenterPoint());
  }

  // Clicking outside the bounds of any window will remove any window selection.
  // However, the layer remains parented to the same root window.
  event_generator->MoveMouseTo(win1->GetBoundsInScreen().bottom_center() +
                               gfx::Vector2d(20, 20));
  event_generator->ClickLeftButton();
  EXPECT_FALSE(controller->selected_window());
  EXPECT_FALSE(controller->pie_menu_widget()->IsVisible());
}

TEST_F(WmModeTests, MoveToDeskSubMenu) {
  // Start with 2 desks.
  UpdateDisplay("800x700");
  NewDesk();
  auto* controller = WmModeController::Get();
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());
  // The pie menu should be created but not visible.
  ASSERT_TRUE(controller->pie_menu_widget());
  EXPECT_FALSE(controller->pie_menu_widget()->IsVisible());

  // There should be 2 buttons on the move-to-desk sub menu, one for each
  // available desk.
  EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId)
                ->button_count(),
            2u);

  // Add 2 new desks, the menu should be updated.
  NewDesk();
  NewDesk();
  EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId)
                ->button_count(),
            4u);

  // Removing a desk will also be observed.
  auto* desks_controller = DesksController::Get();
  RemoveDesk(desks_controller->desks().back().get());
  EXPECT_EQ(GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId)
                ->button_count(),
            3u);

  // Only the active desk button is disabled, since you can't move the window to
  // it.
  EXPECT_FALSE(
      GetPieMenuButtonById(WmModeController::kDeskButtonIdStart)->GetEnabled());
  EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 1)
                  ->GetEnabled());
  EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 2)
                  ->GetEnabled());

  // Switching desks will turn off WM Mode.
  ActivateDesk(desks_controller->desks().back().get());
  EXPECT_FALSE(controller->is_active());

  // Activate WM Mode again, and expect that the pie menu's active desk button
  // is disabled.
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());

  EXPECT_TRUE(
      GetPieMenuButtonById(WmModeController::kDeskButtonIdStart)->GetEnabled());
  EXPECT_TRUE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 1)
                  ->GetEnabled());
  EXPECT_FALSE(GetPieMenuButtonById(WmModeController::kDeskButtonIdStart + 2)
                   ->GetEnabled());
}

TEST_F(WmModeTests, MoveWindowToDeskFromPieMenu) {
  // Start with 2 desks.
  UpdateDisplay("800x700");
  NewDesk();
  auto window = CreateAppWindow(gfx::Rect(400, 400));
  auto* controller = WmModeController::Get();
  controller->Toggle();
  EXPECT_TRUE(controller->is_active());
  auto* event_generator = GetEventGenerator();
  event_generator->MoveMouseToCenterOf(window.get());
  event_generator->ClickLeftButton();
  EXPECT_EQ(controller->selected_window(), window.get());
  EXPECT_TRUE(controller->pie_menu_widget()->IsVisible());

  // The move-to-desk sub menu is initially hidden.
  auto* move_to_desk_sub_menu =
      GetPieSubMenuContainerView(WmModeController::kMoveToDeskButtonId);
  EXPECT_FALSE(move_to_desk_sub_menu->GetVisible());
  // Once the associated menu button is pressed, the sub menu is shown.
  LeftClickPieMenuButton(WmModeController::kMoveToDeskButtonId);
  EXPECT_TRUE(move_to_desk_sub_menu->GetVisible());

  // Clicking on the button for the second desk will move the window to that
  // desk.
  LeftClickPieMenuButton(WmModeController::kDeskButtonIdStart + 1);
  EXPECT_TRUE(base::Contains(DesksController::Get()->desks().back()->windows(),
                             window.get()));

  // The pie menu should have been hidden, and the selected window is now
  // cleared.
  EXPECT_TRUE(controller->is_active());
  EXPECT_FALSE(controller->selected_window());
  EXPECT_FALSE(controller->pie_menu_widget()->IsVisible());
}

}  // namespace ash