// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ui/frame/caption_buttons/frame_size_button.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/frame/snap_controller_impl.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_test_util.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/phantom_window_controller.h"
#include "base/check_op.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "chromeos/ui/frame/multitask_menu/multitask_button.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_metrics.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_view_test_api.h"
#include "chromeos/ui/frame/multitask_menu/split_button_view.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/caption_button_layout_constants.h"
#include "ui/views/window/frame_caption_button.h"
#include "ui/views/window/vector_icons/vector_icons.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
using ::chromeos::FrameCaptionButtonContainerView;
using ::chromeos::FrameSizeButton;
using ::chromeos::MultitaskButton;
using ::chromeos::MultitaskMenu;
using ::chromeos::MultitaskMenuEntryType;
using ::chromeos::MultitaskMenuViewTestApi;
using ::chromeos::SplitButtonView;
using ::chromeos::WindowStateType;
constexpr char kMultitaskMenuBubbleWidgetName[] = "MultitaskMenuBubbleWidget";
class TestWidgetDelegate : public views::WidgetDelegateView {
public:
explicit TestWidgetDelegate(bool resizable) {
SetCanMaximize(true);
SetCanMinimize(true);
SetCanResize(resizable);
}
TestWidgetDelegate(const TestWidgetDelegate&) = delete;
TestWidgetDelegate& operator=(const TestWidgetDelegate&) = delete;
~TestWidgetDelegate() override = default;
FrameCaptionButtonContainerView* caption_button_container() {
return caption_button_container_;
}
private:
// Overridden from views::View:
void Layout(PassKey) override {
// Right align the caption button container.
gfx::Size preferred_size = caption_button_container_->GetPreferredSize();
caption_button_container_->SetBounds(width() - preferred_size.width(), 0,
preferred_size.width(),
preferred_size.height());
}
void ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) override {
if (details.is_add && details.child == this) {
caption_button_container_ =
new FrameCaptionButtonContainerView(GetWidget());
// Set images for the button icons and assign the default caption button
// size.
caption_button_container_->SetButtonSize(
views::GetCaptionButtonLayoutSize(
views::CaptionButtonLayoutSize::kNonBrowserCaption));
caption_button_container_->SetButtonImage(
views::CAPTION_BUTTON_ICON_MINIMIZE,
views::kWindowControlMinimizeIcon);
caption_button_container_->SetButtonImage(views::CAPTION_BUTTON_ICON_MENU,
chromeos::kFloatWindowIcon);
caption_button_container_->SetButtonImage(
views::CAPTION_BUTTON_ICON_CLOSE, views::kWindowControlCloseIcon);
caption_button_container_->SetButtonImage(
views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
chromeos::kWindowControlLeftSnappedIcon);
caption_button_container_->SetButtonImage(
views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
chromeos::kWindowControlRightSnappedIcon);
caption_button_container()->SetButtonImage(
views::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE,
views::kWindowControlMaximizeIcon);
AddChildView(caption_button_container_.get());
}
}
// Not owned.
raw_ptr<FrameCaptionButtonContainerView> caption_button_container_;
};
class FrameSizeButtonTest : public AshTestBase {
public:
FrameSizeButtonTest() {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kSnapGroup,
features::kOsSettingsRevampWayfinding},
/*disabled_features=*/{});
}
explicit FrameSizeButtonTest(bool resizable) : resizable_(resizable) {}
FrameSizeButtonTest(const FrameSizeButtonTest&) = delete;
FrameSizeButtonTest& operator=(const FrameSizeButtonTest&) = delete;
~FrameSizeButtonTest() override = default;
// Returns the center point of |view| in screen coordinates.
gfx::Point CenterPointInScreen(views::View* view) {
return view->GetBoundsInScreen().CenterPoint();
}
// Returns true if the window has |state_type|.
bool HasStateType(WindowStateType state_type) const {
return window_state()->GetStateType() == state_type;
}
// Returns true if all three buttons are in the normal state.
bool AllButtonsInNormalState() const {
return minimize_button_->GetState() == views::Button::STATE_NORMAL &&
size_button_->GetState() == views::Button::STATE_NORMAL &&
close_button_->GetState() == views::Button::STATE_NORMAL;
}
// Creates a widget with |delegate|. The returned widget takes ownership of
// |delegate|.
views::Widget* CreateWidget(views::WidgetDelegate* delegate) {
views::Widget* widget = new views::Widget;
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW);
params.delegate = delegate;
params.bounds = gfx::Rect(10, 10, 100, 100);
params.context = GetContext();
widget->Init(std::move(params));
widget->Show();
return widget;
}
// AshTestBase overrides:
void SetUp() override {
AshTestBase::SetUp();
widget_delegate_ = new TestWidgetDelegate(resizable_);
widget_ = CreateWidget(widget_delegate_);
widget_->GetNativeWindow()->SetProperty(chromeos::kAppTypeKey,
chromeos::AppType::BROWSER);
window_state_ = WindowState::Get(widget_->GetNativeWindow());
FrameCaptionButtonContainerView::TestApi test(
widget_delegate_->caption_button_container());
minimize_button_ = test.minimize_button();
size_button_ = test.size_button();
static_cast<FrameSizeButton*>(size_button_)
->set_long_tap_delay_for_testing(base::Milliseconds(0));
close_button_ = test.close_button();
}
WindowState* window_state() { return window_state_; }
const WindowState* window_state() const { return window_state_; }
views::Widget* GetWidget() { return widget_; }
views::FrameCaptionButton* minimize_button() { return minimize_button_; }
views::FrameCaptionButton* size_button() { return size_button_; }
views::FrameCaptionButton* close_button() { return close_button_; }
TestWidgetDelegate* widget_delegate() { return widget_delegate_; }
private:
base::test::ScopedFeatureList scoped_feature_list_;
// Not owned.
raw_ptr<WindowState, DanglingUntriaged> window_state_;
raw_ptr<views::Widget, DanglingUntriaged> widget_;
raw_ptr<views::FrameCaptionButton, DanglingUntriaged> minimize_button_;
raw_ptr<views::FrameCaptionButton, DanglingUntriaged> size_button_;
raw_ptr<views::FrameCaptionButton, DanglingUntriaged> close_button_;
raw_ptr<TestWidgetDelegate, DanglingUntriaged> widget_delegate_;
bool resizable_ = true;
};
} // namespace
// Tests that pressing the left mouse button or tapping down on the size button
// puts the button into the pressed state.
TEST_F(FrameSizeButtonTest, PressedState) {
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressTouchId(3);
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
generator->ReleaseTouchId(3);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
}
// Tests that clicking on the size button toggles between the maximized and
// normal state.
TEST_F(FrameSizeButtonTest, ClickSizeButtonTogglesMaximize) {
EXPECT_FALSE(window_state()->IsMaximized());
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->ClickLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(window_state()->IsMaximized());
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->ClickLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(window_state()->IsMaximized());
generator->GestureTapAt(CenterPointInScreen(size_button()));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(window_state()->IsMaximized());
generator->GestureTapAt(CenterPointInScreen(size_button()));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(window_state()->IsMaximized());
}
// Test that clicking + dragging to a button adjacent to the size button snaps
// the window left or right.
TEST_F(FrameSizeButtonTest, ButtonDrag) {
EXPECT_TRUE(window_state()->IsNormalStateType());
// 1) Test by dragging the mouse.
// Snap right.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
generator->MoveMouseTo(CenterPointInScreen(close_button()));
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kSecondarySnapped));
// Snap left.
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
// 2) Test with scroll gestures.
// Snap right.
generator->GestureScrollSequence(CenterPointInScreen(size_button()),
CenterPointInScreen(close_button()),
base::Milliseconds(100), 3);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kSecondarySnapped));
// Snap left.
generator->GestureScrollSequence(CenterPointInScreen(size_button()),
CenterPointInScreen(minimize_button()),
base::Milliseconds(100), 3);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
// 3) Test with tap gestures.
const float touch_default_radius =
ui::GestureConfiguration::GetInstance()->default_radius();
ui::GestureConfiguration::GetInstance()->set_default_radius(0);
// Snap right.
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressMoveAndReleaseTouchTo(CenterPointInScreen(close_button()));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kSecondarySnapped));
// Snap left.
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressMoveAndReleaseTouchTo(CenterPointInScreen(minimize_button()));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
ui::GestureConfiguration::GetInstance()->set_default_radius(
touch_default_radius);
}
// Test that clicking, dragging, and overshooting the minimize button a bit
// horizontally still snaps the window left.
TEST_F(FrameSizeButtonTest, SnapLeftOvershootMinimize) {
EXPECT_TRUE(window_state()->IsNormalStateType());
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
// Move to the minimize button.
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
// Overshoot the minimize button.
generator->MoveMouseBy(-minimize_button()->width(), 0);
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
}
// Test that right clicking the size button has no effect.
TEST_F(FrameSizeButtonTest, RightMouseButton) {
EXPECT_TRUE(window_state()->IsNormalStateType());
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressRightButton();
generator->ReleaseRightButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(window_state()->IsNormalStateType());
}
// Test that during the waiting to snap mode, if the window's state is changed,
// or the window is put in overview, we should cancel the waiting to snap mode.
TEST_F(FrameSizeButtonTest, CancelSnapTest) {
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
// Press on the size button and drag toward to close buton to enter waiting-
// for-snap mode.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
generator->MoveMouseTo(CenterPointInScreen(close_button()));
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_TRUE(
static_cast<FrameSizeButton*>(size_button())->in_snap_mode_for_testing());
// Maximize the window.
window_state()->Maximize();
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
EXPECT_FALSE(
static_cast<FrameSizeButton*>(size_button())->in_snap_mode_for_testing());
generator->ReleaseLeftButton();
// Test that if window is put in overview, the waiting-to-snap is canceled.
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
generator->MoveMouseTo(CenterPointInScreen(close_button()));
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_TRUE(
static_cast<FrameSizeButton*>(size_button())->in_snap_mode_for_testing());
window_state()->window()->SetProperty(chromeos::kIsShowingInOverviewKey,
true);
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
EXPECT_FALSE(
static_cast<FrameSizeButton*>(size_button())->in_snap_mode_for_testing());
generator->ReleaseLeftButton();
}
// Test that upon releasing the mouse button after having pressed the size
// button
// - The state of all the caption buttons is reset.
// - The icon displayed by all of the caption buttons is reset.
TEST_F(FrameSizeButtonTest, ResetButtonsAfterClick) {
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
EXPECT_TRUE(AllButtonsInNormalState());
// Pressing the size button should result in the size button being pressed and
// the minimize and close button icons changing.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_TRUE(chromeos::kWindowControlLeftSnappedIcon.name ==
minimize_button()->icon_definition_for_test()->name);
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
EXPECT_TRUE(chromeos::kWindowControlRightSnappedIcon.name ==
close_button()->icon_definition_for_test()->name);
// Dragging the mouse over the minimize button should hover the minimize
// button and the minimize and close button icons should stay changed.
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
EXPECT_EQ(views::Button::STATE_HOVERED, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
// Release the mouse, snapping the window left.
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
// None of the buttons should stay pressed and the buttons should have their
// regular icons.
EXPECT_TRUE(AllButtonsInNormalState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
// Repeat test but release button where it does not affect the window's state
// because the code path is different.
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
generator->MoveMouseTo(work_area_bounds_in_screen.bottom_left());
// None of the buttons should be pressed because we are really far away from
// any of the caption buttons. The minimize and close button icons should
// be changed because the mouse is pressed.
EXPECT_TRUE(AllButtonsInNormalState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
// Release the mouse. The window should stay snapped left.
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
// The buttons should stay unpressed and the buttons should now have their
// regular icons.
EXPECT_TRUE(AllButtonsInNormalState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
}
// Test that the size button is pressed whenever the snap left/right buttons
// are hovered.
TEST_F(FrameSizeButtonTest, SizeButtonPressedWhenSnapButtonHovered) {
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
EXPECT_TRUE(AllButtonsInNormalState());
// Pressing the size button should result in the size button being pressed and
// the minimize and close button icons changing.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
// Dragging the mouse over the minimize button (snap left button) should hover
// the minimize button and keep the size button pressed.
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
EXPECT_EQ(views::Button::STATE_HOVERED, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
// Moving the mouse far away from the caption buttons and then moving it over
// the close button (snap right button) should hover the close button and
// keep the size button pressed.
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
generator->MoveMouseTo(work_area_bounds_in_screen.bottom_left());
EXPECT_TRUE(AllButtonsInNormalState());
generator->MoveMouseTo(CenterPointInScreen(close_button()));
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_HOVERED, close_button()->GetState());
}
class FrameSizeButtonTestRTL : public FrameSizeButtonTest {
public:
FrameSizeButtonTestRTL() = default;
FrameSizeButtonTestRTL(const FrameSizeButtonTestRTL&) = delete;
FrameSizeButtonTestRTL& operator=(const FrameSizeButtonTestRTL&) = delete;
~FrameSizeButtonTestRTL() override = default;
void SetUp() override {
original_locale_ = base::i18n::GetConfiguredLocale();
base::i18n::SetICUDefaultLocale("he");
FrameSizeButtonTest::SetUp();
}
void TearDown() override {
FrameSizeButtonTest::TearDown();
base::i18n::SetICUDefaultLocale(original_locale_);
}
private:
std::string original_locale_;
};
// Test that clicking + dragging to a button adjacent to the size button presses
// the correct button and snaps the window to the correct side.
TEST_F(FrameSizeButtonTestRTL, ButtonDrag) {
// In RTL the close button should be left of the size button and the minimize
// button should be right of the size button.
ASSERT_LT(close_button()->GetBoundsInScreen().x(),
size_button()->GetBoundsInScreen().x());
ASSERT_LT(size_button()->GetBoundsInScreen().x(),
minimize_button()->GetBoundsInScreen().x());
// Test initial state.
EXPECT_TRUE(window_state()->IsNormalStateType());
EXPECT_TRUE(AllButtonsInNormalState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
// Pressing the size button should swap the icons of the minimize and close
// buttons to icons for snapping right and for snapping left respectively.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
close_button()->GetIcon());
// Dragging over to the minimize button should press it.
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
EXPECT_EQ(views::Button::STATE_HOVERED, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
// Releasing should snap the window right.
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kSecondarySnapped));
// None of the buttons should stay pressed and the buttons should have their
// regular icons.
EXPECT_TRUE(AllButtonsInNormalState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
}
namespace {
class FrameSizeButtonNonResizableTest : public FrameSizeButtonTest {
public:
FrameSizeButtonNonResizableTest() : FrameSizeButtonTest(false) {}
FrameSizeButtonNonResizableTest(const FrameSizeButtonNonResizableTest&) =
delete;
FrameSizeButtonNonResizableTest& operator=(
const FrameSizeButtonNonResizableTest&) = delete;
~FrameSizeButtonNonResizableTest() override = default;
};
} // namespace
TEST_F(FrameSizeButtonNonResizableTest, NoSnap) {
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
EXPECT_TRUE(AllButtonsInNormalState());
// Pressing the size button should result in the size button being pressed and
// the minimize and close button icons changing.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
}
// FrameSizeButtonPortraitDisplayTest is used to test functionalities to snap
// top and bottom in portrait layout, affecting snap icons.
using FrameSizeButtonPortraitDisplayTest = FrameSizeButtonTest;
// Test that upon pressed the size button should show left and right arrows for
// horizontal snap and upward and downward arrow for vertical snap.
TEST_F(FrameSizeButtonPortraitDisplayTest, SnapButtons) {
UpdateDisplay("600x800");
FrameCaptionButtonContainerView* container =
widget_delegate()->caption_button_container();
views::Widget* widget = widget_delegate()->GetWidget();
chromeos::DefaultFrameHeader frame_header(
widget, widget->non_client_view()->frame_view(), container);
frame_header.LayoutHeader();
EXPECT_EQ(views::CAPTION_BUTTON_ICON_MINIMIZE, minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_CLOSE, close_button()->GetIcon());
EXPECT_TRUE(AllButtonsInNormalState());
// Pressing the size button should result in the size button being pressed and
// the minimize and close button icons changing.
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
EXPECT_EQ(views::Button::STATE_NORMAL, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
const gfx::VectorIcon* left_icon = &chromeos::kWindowControlTopSnappedIcon;
const gfx::VectorIcon* right_icon =
&chromeos::kWindowControlBottomSnappedIcon;
EXPECT_TRUE(left_icon->name ==
minimize_button()->icon_definition_for_test()->name);
EXPECT_TRUE(right_icon->name ==
close_button()->icon_definition_for_test()->name);
// Dragging the mouse over the minimize button should hover the minimize
// button (snap top/left). The minimize and close button icons should stay
// changed.
generator->MoveMouseTo(CenterPointInScreen(minimize_button()));
EXPECT_EQ(views::Button::STATE_HOVERED, minimize_button()->GetState());
EXPECT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
EXPECT_EQ(views::Button::STATE_NORMAL, close_button()->GetState());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_LEFT_TOP_SNAPPED,
minimize_button()->GetIcon());
EXPECT_EQ(views::CAPTION_BUTTON_ICON_RIGHT_BOTTOM_SNAPPED,
close_button()->GetIcon());
// Release the mouse, snapping the window to the primary position.
generator->ReleaseLeftButton();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(HasStateType(WindowStateType::kPrimarySnapped));
}
class MultitaskMenuTest : public FrameSizeButtonTest {
public:
MultitaskMenuTest() = default;
MultitaskMenuTest(const MultitaskMenuTest&) = delete;
MultitaskMenuTest& operator=(const MultitaskMenuTest&) = delete;
~MultitaskMenuTest() override = default;
MultitaskMenu* GetMultitaskMenu() {
views::Widget* widget = static_cast<FrameSizeButton*>(size_button())
->multitask_menu_widget_for_testing();
return widget ? static_cast<MultitaskMenu*>(
widget->widget_delegate()->AsDialogDelegate())
: nullptr;
}
void ShowMultitaskMenu(MultitaskMenuEntryType entry_type =
MultitaskMenuEntryType::kFrameSizeButtonHover) {
ShowAndWaitMultitaskMenuForWindow(
static_cast<FrameSizeButton*>(size_button()), entry_type);
}
aura::Window* window() { return window_state()->window(); }
wm::ActivationClient* activation_client() {
return wm::GetActivationClient(window()->GetRootWindow());
}
};
// Test float button functionality.
TEST_F(MultitaskMenuTest, TestMultitaskMenuFloatFunctionality) {
base::HistogramTester histogram_tester;
EXPECT_TRUE(window_state()->IsNormalStateType());
ui::test::EventGenerator* generator = GetEventGenerator();
window_state()->Deactivate();
ASSERT_NE(activation_client()->GetActiveWindow(), window());
ShowMultitaskMenu();
EXPECT_NE(activation_client()->GetActiveWindow(), window());
generator->MoveMouseTo(CenterPointInScreen(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetFloatButton()));
generator->ClickLeftButton();
EXPECT_TRUE(window_state()->IsFloated());
histogram_tester.ExpectBucketCount(
chromeos::GetActionTypeHistogramName(),
chromeos::MultitaskMenuActionType::kFloatButton, 1);
EXPECT_EQ(activation_client()->GetActiveWindow(), window());
}
// Test Half Button Functionality.
TEST_F(MultitaskMenuTest, TestMultitaskMenuHalfFunctionality) {
base::HistogramTester histogram_tester;
EXPECT_TRUE(window_state()->IsNormalStateType());
window_state()->Deactivate();
ASSERT_NE(activation_client()->GetActiveWindow(), window());
ShowMultitaskMenu();
EXPECT_NE(activation_client()->GetActiveWindow(), window());
LeftClickOn(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
histogram_tester.ExpectBucketCount(
chromeos::GetActionTypeHistogramName(),
chromeos::MultitaskMenuActionType::kHalfSplitButton, 1);
EXPECT_EQ(activation_client()->GetActiveWindow(), window());
}
// Tests that clicking the left side of the half button works as intended for
// RTL setups.
TEST_F(MultitaskMenuTest, HalfButtonRTL) {
UpdateDisplay("800x600");
base::i18n::SetRTLForTesting(true);
ShowMultitaskMenu();
LeftClickOn(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(gfx::Rect(400, 552), GetWidget()->GetWindowBoundsInScreen());
// Overview may start due to faster split screen when the window is snapped.
// Escape overview if it is active, otherwise the key event will be handled in
// `OverviewSession` to exit overview, see `OverviewSession::OnKeyEvent()` for
// more details. Pressing the Alt key below won't reverse the multi-task menu.
if (IsInOverviewSession()) {
PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
}
// Reverse the menu. Test that the left button still snaps to primary.
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_MENU, ui::EF_ALT_DOWN);
ASSERT_TRUE(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetIsReversed());
LeftClickOn(GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(gfx::Rect(266, 552), GetWidget()->GetWindowBoundsInScreen());
}
// Tests that if the display is in secondary layout, pressing the physically
// left side button should snap it to the correct side.
TEST_F(MultitaskMenuTest, HalfButtonSecondaryLayout) {
// Rotate the display 180 degrees so its layout is not primary.
const int64_t display_id =
display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::test::ScopedSetInternalDisplayId set_internal(
Shell::Get()->display_manager(), display_id);
ScreenOrientationControllerTestApi(
Shell::Get()->screen_orientation_controller())
.SetDisplayRotation(display::Display::ROTATE_180,
display::Display::RotationSource::ACTIVE);
ShowMultitaskMenu(MultitaskMenuEntryType::kAccel);
// Click on the left side of the half button. It should be in secondary
// snapped state, because in this orientation secondary snapped is actually
// physically on the left side.
GetEventGenerator()->MoveMouseToInHost(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetBoundsInScreen()
.left_center());
GetEventGenerator()->ClickLeftButton();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
}
// Test Partial Split Button Functionality.
TEST_F(MultitaskMenuTest, TestMultitaskMenuPartialSplit) {
base::HistogramTester histogram_tester;
EXPECT_TRUE(window_state()->IsNormalStateType());
window_state()->Deactivate();
ASSERT_NE(activation_client()->GetActiveWindow(), window());
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
// Verify the metrics initial states.
base::UserActionTester user_action_tester;
EXPECT_EQ(user_action_tester.GetActionCount(
chromeos::kPartialSplitOneThirdUserAction),
0);
EXPECT_EQ(user_action_tester.GetActionCount(
chromeos::kPartialSplitTwoThirdsUserAction),
0);
// Snap to primary with 0.67f screen ratio.
ShowMultitaskMenu();
EXPECT_NE(activation_client()->GetActiveWindow(), window());
LeftClickOn(GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(window_state()->window()->bounds().width(),
std::round(work_area_bounds_in_screen.width() *
chromeos::kTwoThirdSnapRatio));
EXPECT_EQ(user_action_tester.GetActionCount(
chromeos::kPartialSplitTwoThirdsUserAction),
1);
histogram_tester.ExpectBucketCount(
chromeos::GetActionTypeHistogramName(),
chromeos::MultitaskMenuActionType::kPartialSplitButton, 1);
EXPECT_EQ(activation_client()->GetActiveWindow(), window());
// Snap to secondary with 0.33f screen ratio.
ShowMultitaskMenu();
LeftClickOn(GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetRightBottomButton());
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(window_state()->window()->bounds().width(),
std::round(work_area_bounds_in_screen.width() *
chromeos::kOneThirdSnapRatio));
EXPECT_EQ(user_action_tester.GetActionCount(
chromeos::kPartialSplitOneThirdUserAction),
1);
histogram_tester.ExpectBucketCount(
chromeos::GetActionTypeHistogramName(),
chromeos::MultitaskMenuActionType::kPartialSplitButton, 2);
}
// Verify that selecting the 2/3 partial split option from the window layout
// menu correctly updates the snap ratio to 2/3 and snaps the target window to
// occupy two-thirds of the available space. Regression test for
// http://b/356537586.
TEST_F(MultitaskMenuTest, PartialSplitInNonPrimaryDisplay) {
// Update display to be in non-primary landscape mode.
UpdateDisplay("800x600/u");
display::DisplayManager* display_manager = Shell::Get()->display_manager();
const auto& displays = display_manager->active_display_list();
ASSERT_EQ(1U, displays.size());
ASSERT_EQ(chromeos::OrientationType::kLandscapeSecondary,
chromeos::GetDisplayCurrentOrientation(displays[0]));
ShowMultitaskMenu(MultitaskMenuEntryType::kAccel);
const gfx::Point two_thirds_partial_button_center =
GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetLeftTopButton()
->GetBoundsInScreen()
.CenterPoint();
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseToInHost(two_thirds_partial_button_center);
// Verify that the target window snaps at expected snap position with the
// correct snap ratio applied.
event_generator->ClickLeftButton();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_THAT(window_state()->snap_ratio(),
testing::Optional(chromeos::kTwoThirdSnapRatio));
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
EXPECT_NEAR(work_area_bounds_in_screen.width() * chromeos::kTwoThirdSnapRatio,
window_state()->window()->bounds().width(), 1);
}
// Test Full Button Functionality.
TEST_F(MultitaskMenuTest, TestMultitaskMenuFullFunctionality) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(window_state()->IsNormalStateType());
window_state()->Deactivate();
ASSERT_NE(activation_client()->GetActiveWindow(), window());
ShowMultitaskMenu();
EXPECT_NE(activation_client()->GetActiveWindow(), window());
LeftClickOn(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetFullButton());
EXPECT_TRUE(window_state()->IsFullscreen());
histogram_tester.ExpectBucketCount(
chromeos::GetActionTypeHistogramName(),
chromeos::MultitaskMenuActionType::kFullscreenButton, 1);
EXPECT_EQ(activation_client()->GetActiveWindow(), window());
}
TEST_F(MultitaskMenuTest, MultitaskMenuClosesOnTabletMode) {
ShowMultitaskMenu();
ASSERT_TRUE(GetMultitaskMenu());
TabletMode::Get()->SetEnabledForTest(true);
// Closing the widget is done on a post task.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetMultitaskMenu());
}
// Verifies that long touch on the size button shows the multitask menu.
// Regression test for https://crbug.com/1367376.
TEST_F(MultitaskMenuTest, LongTouchShowsMultitaskMenu) {
ASSERT_TRUE(size_button());
base::HistogramTester histogram_tester;
// Touch until the multitask bubble shows up. This would time out if long
// touch was not working.
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
std::string(kMultitaskMenuBubbleWidgetName));
GetEventGenerator()->PressTouch(
size_button()->GetBoundsInScreen().CenterPoint());
views::Widget* bubble_widget = waiter.WaitIfNeededAndGet();
EXPECT_TRUE(bubble_widget);
histogram_tester.ExpectBucketCount(
chromeos::GetEntryTypeHistogramName(),
MultitaskMenuEntryType::kFrameSizeButtonLongTouch, 1);
}
// Verifies that metrics are recorded properly for clamshell entry points.
TEST_F(MultitaskMenuTest, EntryTypeHistogram) {
base::HistogramTester histogram_tester;
// Check that mouse hover increments the correct bucket.
GetEventGenerator()->MoveMouseTo(CenterPointInScreen(size_button()));
histogram_tester.ExpectBucketCount(
chromeos::GetEntryTypeHistogramName(),
MultitaskMenuEntryType::kFrameSizeButtonHover, 1);
// Check that long press increments the correct bucket.
GetEventGenerator()->MoveMouseTo(CenterPointInScreen(size_button()));
GetEventGenerator()->PressLeftButton();
histogram_tester.ExpectBucketCount(
chromeos::GetEntryTypeHistogramName(),
MultitaskMenuEntryType::kFrameSizeButtonLongPress, 1);
// Check that the accelerator increments the correct bucket.
// Create an active window for the toggle menu to work.
auto window = CreateTestWindow();
PressAndReleaseKey(ui::VKEY_Z, ui::EF_COMMAND_DOWN);
histogram_tester.ExpectBucketCount(chromeos::GetEntryTypeHistogramName(),
MultitaskMenuEntryType::kAccel, 1);
// Check total counts for each histogram to ensure calls aren't counted in
// multiple buckets.
histogram_tester.ExpectTotalCount(chromeos::GetEntryTypeHistogramName(), 3);
}
// Tests that we do not create a new widget when hovering on the size button
// when the multitask menu is already opened.
TEST_F(MultitaskMenuTest, HoverWhenMenuAlreadyShown) {
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
// Ensure the mouse is not already on the size button then move it back on.
// The menu should be the same one opened beforehand.
GetEventGenerator()->MoveMouseTo(
size_button()->GetBoundsInScreen().bottom_right() + gfx::Vector2d(2, 2));
GetEventGenerator()->MoveMouseTo(CenterPointInScreen(size_button()));
EXPECT_EQ(multitask_menu, GetMultitaskMenu());
}
TEST_F(MultitaskMenuTest, CloseOnClickOutside) {
// Snap the window to half so we can click outside the window bounds.
ShowMultitaskMenu();
LeftClickOn(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
// Click on a point outside the menu on the screen below.
gfx::Point offset_point(multitask_menu->multitask_menu_view()
->GetBoundsInScreen()
.bottom_right());
offset_point.Offset(10, 10);
DCHECK(!GetWidget()->GetWindowBoundsInScreen().Contains(offset_point));
GetEventGenerator()->MoveMouseTo(offset_point);
GetEventGenerator()->ClickLeftButton();
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(GetMultitaskMenu());
}
// Tests that moving the mouse outside the menu will close the menu, if opened
// via hovering on the frame size button.
TEST_F(MultitaskMenuTest, MoveMouseOutsideMenu) {
chromeos::MultitaskMenuView::SetSkipMouseOutDelayForTesting(true);
// Simulate opening the menu by moving the mouse to the frame size button and
// opening the menu.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(
size_button()->GetBoundsInScreen().CenterPoint());
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
event_generator->MoveMouseTo(
multitask_menu->GetBoundsInScreen().CenterPoint());
// Widget is closed with a post task.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetMultitaskMenu());
event_generator->MoveMouseTo(gfx::Point(1, 1));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetMultitaskMenu());
// Open the menu using the accelerator.
event_generator->MoveMouseTo(
size_button()->GetBoundsInScreen().CenterPoint());
ShowMultitaskMenu(MultitaskMenuEntryType::kAccel);
// Test that the menu remains open if we move outside when using the
// accelerator.
event_generator->MoveMouseTo(gfx::Point(1, 1));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(GetMultitaskMenu());
}
// Tests that window accelerators, e.g. minimize, still work when the multitask
// menu is open.
TEST_F(MultitaskMenuTest, MinimizeWhenMenuShown) {
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_OEM_MINUS, ui::EF_ALT_DOWN);
ASSERT_TRUE(window_state()->IsMinimized());
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(GetMultitaskMenu());
PressAndReleaseKey(ui::VKEY_OEM_MINUS, ui::EF_ALT_DOWN);
ASSERT_FALSE(window_state()->IsMinimized());
EXPECT_TRUE(GetWidget()->GetNativeWindow()->IsVisible());
EXPECT_FALSE(GetMultitaskMenu());
}
// Tests that the partial button is horizontally flipped when the `Alt` key is
// pressed while the menu is shown.
TEST_F(MultitaskMenuTest, ReversePartialButton) {
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
// Reverse the menu. Test that the left button snaps to 1/3.
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_MENU, ui::EF_ALT_DOWN);
ASSERT_TRUE(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetIsReversed());
LeftClickOn(GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetLeftTopButton());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(std::floor(work_area_bounds_in_screen.width() *
chromeos::kOneThirdSnapRatio),
GetWidget()->GetWindowBoundsInScreen().width());
// Overview may start due to faster split screen when the window is
// snapped. Escape overview if it is active, otherwise the key event will be
// handled in `OverviewSession` to exit overview, see
// `OverviewSession::OnKeyEvent()` for more details. Pressing the Alt key
// below won't reverse the multi-task menu.
if (IsInOverviewSession()) {
PressAndReleaseKey(ui::VKEY_ESCAPE, ui::EF_NONE);
}
// Reverse the menu. Test that the right button snaps to 2/3.
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_MENU, ui::EF_ALT_DOWN);
ASSERT_TRUE(
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetIsReversed());
LeftClickOn(GetMultitaskMenu()
->multitask_menu_view()
->partial_button()
->GetRightBottomButton());
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(std::ceil(work_area_bounds_in_screen.width() *
chromeos::kTwoThirdSnapRatio),
GetWidget()->GetWindowBoundsInScreen().width());
}
// Tests that the float button is horizontally flipped when the `Alt` key is
// pressed while the menu is shown.
TEST_F(MultitaskMenuTest, ReverseFloatButton) {
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
const gfx::Rect original_bounds = window_state()->window()->bounds();
// Reverse the menu and press the float button. Test that the window is
// floated and is roughly on the left edge (there is some padding).
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_MENU, ui::EF_ALT_DOWN);
MultitaskMenuViewTestApi test_api(GetMultitaskMenu()->multitask_menu_view());
ASSERT_TRUE(test_api.GetIsReversed());
LeftClickOn(test_api.GetFloatButton());
EXPECT_EQ(WindowStateType::kFloated, window_state()->GetStateType());
EXPECT_EQ(
work_area_bounds_in_screen.x() + chromeos::wm::kFloatedWindowPaddingDp,
GetWidget()->GetWindowBoundsInScreen().x());
// Tests that the float button unfloats if reversed and the window is already
// floated.
ShowMultitaskMenu();
PressAndReleaseKey(ui::VKEY_MENU, ui::EF_ALT_DOWN);
MultitaskMenuViewTestApi test_api2(GetMultitaskMenu()->multitask_menu_view());
ASSERT_TRUE(test_api2.GetIsReversed());
LeftClickOn(test_api2.GetFloatButton());
EXPECT_EQ(WindowStateType::kNormal, window_state()->GetStateType());
EXPECT_EQ(original_bounds, window_state()->window()->bounds());
}
// Tests that pressing on the size button and then dragging and releasing on a
// multitask menu button will trigger it.
TEST_F(MultitaskMenuTest, PressOnSizeButtonReleaseOnMultitaskMenu) {
// First assert that all four buttons are visible with the display size and
// window we are given. We'll need them later and users who add buttons later
// will need to update this test.
ShowMultitaskMenu();
ASSERT_EQ(4u, GetMultitaskMenu()->multitask_menu_view()->children().size());
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Functions to be used for both touch and mouse testing.
auto move_to_center = [event_generator](const views::View* view, bool touch) {
const gfx::Point& screen_location = view->GetBoundsInScreen().CenterPoint();
touch ? event_generator->MoveTouch(screen_location)
: event_generator->MoveMouseTo(screen_location);
};
auto press = [event_generator](bool touch) {
touch ? event_generator->PressTouch() : event_generator->PressLeftButton();
};
auto release = [event_generator](bool touch) {
touch ? event_generator->ReleaseTouch()
: event_generator->ReleaseLeftButton();
};
// Test pressing on the size button, dragging to snap left and float buttons,
// for both mouse and touch.
for (bool touch : {false, true}) {
SCOPED_TRACE(
base::StringPrintf("Testing state: %s", touch ? "touch" : "mouse"));
// Move to the size button and long press to show the multitask menu.
move_to_center(size_button(), touch);
{
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
std::string(kMultitaskMenuBubbleWidgetName));
press(touch);
waiter.WaitIfNeededAndGet();
}
// Without releasing, drag to the left button and release. Test that we are
// in snapped state.
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
views::Button* left_half_button =
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton();
move_to_center(left_half_button, touch);
EXPECT_EQ(views::Button::STATE_HOVERED, left_half_button->GetState());
release(touch);
EXPECT_TRUE(window_state()->IsSnapped());
// Move back to the size button and long press again to show the multitask
// menu.
move_to_center(size_button(), touch);
{
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
std::string(kMultitaskMenuBubbleWidgetName));
press(touch);
waiter.WaitIfNeededAndGet();
}
// Without releasing, drag to the float button and release. Test that we are
// in float state.
multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
views::Button* float_button =
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetFloatButton();
move_to_center(float_button, touch);
EXPECT_EQ(views::Button::STATE_HOVERED, float_button->GetState());
release(touch);
EXPECT_TRUE(window_state()->IsFloated());
}
}
// Tests that if the window is right snapped, and we try to fullscreen the
// window via touch-dragging the multitask menu, the window is properly
// fullscreened. Regression test for http://b/304437185.
TEST_F(MultitaskMenuTest, FullscreenFromTouchMultitaskMenu) {
const WindowSnapWMEvent snap_secondary(WM_EVENT_SNAP_SECONDARY);
window_state()->OnWMEvent(&snap_secondary);
ASSERT_TRUE(window_state()->IsSnapped());
// Long press on the size button until the multitask menu is shown.
ui::test::EventGenerator* event_generator = GetEventGenerator();
views::NamedWidgetShownWaiter waiter(
views::test::AnyWidgetTestPasskey{},
std::string(kMultitaskMenuBubbleWidgetName));
event_generator->PressTouch(size_button()->GetBoundsInScreen().CenterPoint());
waiter.WaitIfNeededAndGet();
// Without releasing, drag to the full button and release. Test that we are
// in fullscreen state.
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
event_generator->MoveTouch(
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetFullButton()
->GetBoundsInScreen()
.CenterPoint());
event_generator->ReleaseTouch();
EXPECT_TRUE(window_state()->IsFullscreen());
}
// Tests that focus traversal with the tab and arrow keys works as expected.
TEST_F(MultitaskMenuTest, TabAndArrowKeyTraversal) {
// First assert that all four buttons are visible with the display size and
// window we are given.
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
ASSERT_TRUE(multitask_menu);
ASSERT_EQ(4u, multitask_menu->multitask_menu_view()->children().size());
// Nothing is focused initially.
views::FocusManager* focus_manager =
multitask_menu->GetWidget()->GetFocusManager();
EXPECT_FALSE(focus_manager->GetFocusedView());
// Press tab. The left button of the half button is focused.
views::Button* left_half_button =
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton();
PressAndReleaseKey(ui::VKEY_TAB);
EXPECT_EQ(left_half_button, focus_manager->GetFocusedView());
// Press shift+tab. The last button (float button) is focused.
views::Button* float_button =
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetFloatButton();
PressAndReleaseKey(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_EQ(float_button, focus_manager->GetFocusedView());
// Press right and left arrow keys. They should work like tab and shift+tab,
// respectively.
PressAndReleaseKey(ui::VKEY_RIGHT);
EXPECT_EQ(left_half_button, focus_manager->GetFocusedView());
PressAndReleaseKey(ui::VKEY_LEFT);
EXPECT_EQ(float_button, focus_manager->GetFocusedView());
// Pressing up/down keys do not affect focus.
PressAndReleaseKey(ui::VKEY_UP);
EXPECT_EQ(float_button, focus_manager->GetFocusedView());
PressAndReleaseKey(ui::VKEY_DOWN);
EXPECT_EQ(float_button, focus_manager->GetFocusedView());
}
// Tests that the menu always fits the display work area.
TEST_F(MultitaskMenuTest, AdjustedMenuBounds) {
// Position the window so that the size button is slightly offscreen.
const gfx::Rect work_area_bounds_in_screen =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
GetWidget()->SetBounds(
gfx::Rect(gfx::Point(work_area_bounds_in_screen.right() - 50, 0),
GetWidget()->GetWindowBoundsInScreen().size()));
ASSERT_FALSE(
work_area_bounds_in_screen.Contains(size_button()->GetBoundsInScreen()));
// Hover over the visible part of the size button. Test that the menu fits
// onscreen.
ShowMultitaskMenu();
EXPECT_TRUE(work_area_bounds_in_screen.Contains(
GetMultitaskMenu()->GetBoundsInScreen()));
}
class SnapGroupFrameSizeButtonTest : public MultitaskMenuTest {
public:
SnapGroupFrameSizeButtonTest() : scoped_feature_list_(features::kSnapGroup) {}
SnapGroupFrameSizeButtonTest(const SnapGroupFrameSizeButtonTest&) = delete;
SnapGroupFrameSizeButtonTest& operator=(const SnapGroupFrameSizeButtonTest&) =
delete;
~SnapGroupFrameSizeButtonTest() override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that long press caption button to show snap phantom bounds are updated.
TEST_F(SnapGroupFrameSizeButtonTest, SnapCaptionButton) {
EXPECT_EQ(views::Button::STATE_NORMAL, size_button()->GetState());
// Create an opposite snapped window with non-default snap ratio.
std::unique_ptr<aura::Window> w1(CreateAppWindow());
const WindowSnapWMEvent snap_primary(
WM_EVENT_SNAP_PRIMARY, chromeos::kTwoThirdSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
WindowState::Get(w1.get())->OnWMEvent(&snap_primary);
// Press on the size button and drag toward the close button to show the snap
// phantom bounds.
wm::ActivateWindow(GetWidget()->GetNativeWindow());
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseTo(CenterPointInScreen(size_button()));
generator->PressLeftButton();
generator->MoveMouseTo(CenterPointInScreen(close_button()));
ASSERT_EQ(views::Button::STATE_PRESSED, size_button()->GetState());
ASSERT_TRUE(
static_cast<FrameSizeButton*>(size_button())->in_snap_mode_for_testing());
auto* snap_controller =
static_cast<SnapControllerImpl*>(chromeos::SnapController::Get());
ASSERT_TRUE(snap_controller);
// Test the phantom bounds reflect the opposite snapped `w1`.
const gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
gfx::Rect expected_bounds(work_area);
expected_bounds.Subtract(w1->GetBoundsInScreen());
EXPECT_TRUE(expected_bounds.ApproximatelyEqual(
snap_controller->phantom_window_controller_for_testing()
->GetTargetWindowBounds(),
/*tolerance=*/kSplitviewDividerShortSideLength / 2));
}
// Tests that when a snap group with a partially occluding window is re-snapped
// via the layout menu, we do not start partial overview. See http://b/348068768
// for context.
TEST_F(SnapGroupFrameSizeButtonTest, ReSnapViaWindowLayoutMenu) {
UpdateDisplay("800x600");
// Create a snap group with `window`, whose frame contains the multitask menu,
// and an `opposite` snapped window.
aura::Window* window = window_state()->window();
std::unique_ptr<aura::Window> opposite(CreateAppWindow());
const WindowSnapWMEvent snap_primary(
WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
window_state()->OnWMEvent(&snap_primary);
const WindowSnapWMEvent snap_secondary(
WM_EVENT_SNAP_SECONDARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
WindowState::Get(opposite.get())->OnWMEvent(&snap_secondary);
auto* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(
snap_group_controller->AreWindowsInSnapGroup(window, opposite.get()));
// Create a partially occluding window on top of `opposite`.
std::unique_ptr<aura::Window> occlude(
CreateAppWindow(gfx::Rect(410, 10, 200, 200)));
ASSERT_TRUE(
opposite->GetBoundsInScreen().Contains(occlude->GetBoundsInScreen()));
// Hover to show the multitask menu on `window`.
ShowMultitaskMenu();
MultitaskMenu* multitask_menu = GetMultitaskMenu();
views::Button* left_half_button =
MultitaskMenuViewTestApi(multitask_menu->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton();
// Click on the snap button to re-snap `window`. Test we don't start overview
// and recall the windows to the front.
LeftClickOn(left_half_button);
ASSERT_FALSE(IsInOverviewSession());
EXPECT_TRUE(SnapGroupController::Get()->AreWindowsInSnapGroup(
window, opposite.get()));
EXPECT_TRUE(window_util::IsStackedBelow(occlude.get(), window));
EXPECT_TRUE(window_util::IsStackedBelow(occlude.get(), opposite.get()));
}
// Tests that re-snapping to the opposite side via the window layout menu starts
// partial overview. Regression test for http://b/349892870.
TEST_F(SnapGroupFrameSizeButtonTest, ReSnapToOppositeSide) {
UpdateDisplay("800x600");
// Create a snap group with `window`, whose frame contains the multitask menu,
// and `window2`.
aura::Window* window = window_state()->window();
SnapOneTestWindow(window, chromeos::WindowStateType::kPrimarySnapped,
chromeos::kTwoThirdSnapRatio);
std::unique_ptr<aura::Window> window2(CreateAppWindow());
SnapOneTestWindow(window2.get(), chromeos::WindowStateType::kSecondarySnapped,
chromeos::kOneThirdSnapRatio);
auto* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(
snap_group_controller->AreWindowsInSnapGroup(window, window2.get()));
// Snap `window` to the right via the layout menu.
ShowMultitaskMenu();
views::Button* right_half_button =
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetRightBottomButton();
LeftClickOn(right_half_button);
VerifySplitViewOverviewSession(window);
EXPECT_TRUE(GetOverviewSession()->IsWindowInOverview(window2.get()));
// Snap `window` to the left via the layout menu.
ShowMultitaskMenu();
views::Button* left_half_button =
MultitaskMenuViewTestApi(GetMultitaskMenu()->multitask_menu_view())
.GetHalfButton()
->GetLeftTopButton();
LeftClickOn(left_half_button);
VerifySplitViewOverviewSession(window);
EXPECT_TRUE(GetOverviewSession()->IsWindowInOverview(window2.get()));
}
} // namespace ash