// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/utils/haptics_util.h"
#include <vector>
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/utility/haptics_tracking_test_input_controller.h"
#include "ash/wm/desks/desk_animation_impl.h"
#include "ash/wm/desks/desk_mini_view.h"
#include "ash/wm/desks/desks_constants.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_histogram_enums.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/desks/root_window_desk_switch_animator_test_api.h"
#include "ash/wm/gestures/wm_gesture_handler.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/workspace/workspace_window_resizer_test_api.h"
#include "chromeos/ui/frame/multitask_menu/multitask_button.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/window.h"
#include "ui/events/devices/haptic_touchpad_effects.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
namespace ash {
using ui::HapticTouchpadEffect;
using ui::HapticTouchpadEffectStrength;
using HapticsUtilTest = AshTestBase;
// Test haptic feedback with all effect/strength combinations.
TEST_F(HapticsUtilTest, HapticFeedbackBasic) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
std::vector<HapticTouchpadEffect> effects = {
HapticTouchpadEffect::kSnap, HapticTouchpadEffect::kKnock,
HapticTouchpadEffect::kTick, HapticTouchpadEffect::kPress,
HapticTouchpadEffect::kRelease,
};
std::vector<HapticTouchpadEffectStrength> strengths = {
HapticTouchpadEffectStrength::kLow,
HapticTouchpadEffectStrength::kMedium,
HapticTouchpadEffectStrength::kHigh,
};
for (HapticTouchpadEffect effect : effects) {
for (HapticTouchpadEffectStrength strength : strengths) {
for (int count = 0; count < 16; count++) {
chromeos::haptics_util::PlayHapticTouchpadEffect(effect, strength);
EXPECT_EQ(count + 1,
input_controller->GetSentHapticCount(effect, strength));
}
}
}
}
// Test haptic feedback for normal window snapping. This covers drag to snap
// primary/secondary/maximize.
TEST_F(HapticsUtilTest, HapticFeedbackForNormalWindowSnap) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
UpdateDisplay("800x600");
gfx::Rect bounds(200, 200, 300, 300);
std::unique_ptr<aura::Window> window = CreateTestWindow(bounds);
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Each element in the vector represents a test case. The first in the pair is
// the drag target point, which is used to trigger window snapping, and the
// second is the expected window bounds after drag.
std::vector<std::pair<gfx::Point, gfx::Rect>> test_cases = {
{{0, 220}, {0, 0, 400, 552}},
{{800, 220}, {400, 0, 400, 552}},
{{350, 0}, {0, 0, 800, 552}},
};
// Drag by touch should not trigger haptic feedback.
for (size_t i = 0; i < test_cases.size(); i++) {
std::pair<gfx::Point, gfx::Rect> test_case = test_cases[i];
window->SetBounds(bounds);
gfx::Point start =
window->GetBoundsInScreen().top_center() + gfx::Vector2d(0, 20);
event_generator->set_current_screen_location(start);
event_generator->PressTouch();
event_generator->MoveTouch(test_case.first);
EXPECT_EQ(0, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseTouch();
EXPECT_EQ(test_case.second, window->GetBoundsInScreen());
}
// Drag by touchpad/mouse should trigger haptic feedback.
for (size_t i = 0; i < test_cases.size(); i++) {
std::pair<gfx::Point, gfx::Rect> test_case = test_cases[i];
window->SetBounds(bounds);
gfx::Point start =
window->GetBoundsInScreen().top_center() + gfx::Vector2d(0, 20);
event_generator->set_current_screen_location(start);
event_generator->PressLeftButton();
event_generator->MoveMouseTo(test_case.first);
auto& dwell_countdown_timer =
WorkspaceWindowResizerTestApi().GetDwellCountdownTimer();
if (dwell_countdown_timer.IsRunning()) {
dwell_countdown_timer.FireNow();
}
EXPECT_EQ((int)i + 1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseLeftButton();
EXPECT_EQ(test_case.second, window->GetBoundsInScreen());
}
}
// Test haptic feedback for overview window snapping. This covers drag to split
// in overview.
TEST_F(HapticsUtilTest, HapticFeedbackForOverviewWindowSnap) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
OverviewController* overview_controller = Shell::Get()->overview_controller();
UpdateDisplay("800x600");
std::unique_ptr<aura::Window> window =
CreateTestWindow(gfx::Rect(200, 200, 300, 300));
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Each element in the vector represents a test case. The first in the pair is
// the drag target point, which is used to trigger window snapping, and the
// second is the expected window bounds after drag.
std::vector<std::pair<gfx::Point, gfx::Rect>> test_cases = {
{{0, 300}, {0, 0, 400, 552}},
{{800, 300}, {400, 0, 400, 552}},
};
// Drag by touch should not trigger haptic feedback.
for (size_t i = 0; i < test_cases.size(); i++) {
std::pair<gfx::Point, gfx::Rect> test_case = test_cases[i];
EnterOverview();
auto* overview_item =
overview_controller->overview_session()->GetOverviewItemForWindow(
window.get());
event_generator->set_current_screen_location(
gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()));
event_generator->PressTouch();
event_generator->MoveTouch(test_case.first);
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_EQ(0, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseTouch();
EXPECT_FALSE(overview_controller->InOverviewSession());
EXPECT_EQ(test_case.second, window->GetBoundsInScreen());
}
// Drag by touchpad/mouse should trigger haptic feedback.
for (size_t i = 0; i < test_cases.size(); i++) {
std::pair<gfx::Point, gfx::Rect> test_case = test_cases[i];
EnterOverview();
auto* overview_item =
overview_controller->overview_session()->GetOverviewItemForWindow(
window.get());
event_generator->set_current_screen_location(
gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()));
event_generator->PressLeftButton();
event_generator->MoveMouseTo(test_case.first);
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_EQ((int)i + 1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseLeftButton();
EXPECT_FALSE(overview_controller->InOverviewSession());
EXPECT_EQ(test_case.second, window->GetBoundsInScreen());
}
}
// Test haptic feedback for off limits desk switching, e.g. swiping left from
// the first desk and swiping right from the last desk.
TEST_F(HapticsUtilTest, HapticFeedbackForDeskSwitchingOffLimits) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
auto* desk_controller = DesksController::Get();
// Make sure to start with two desks.
NewDesk();
EXPECT_EQ(2u, desk_controller->desks().size());
EXPECT_EQ(0, desk_controller->GetActiveDeskIndex());
EXPECT_EQ(
input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock, HapticTouchpadEffectStrength::kMedium),
0);
// Swipe from `desk 0` to `desk 1` should not trigger `kKnock` effect.
ScrollToSwitchDesks(/*scroll_left=*/false, GetEventGenerator());
EXPECT_EQ(1, desk_controller->GetActiveDeskIndex());
EXPECT_EQ(0, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock,
HapticTouchpadEffectStrength::kMedium));
// Swipe from `desk 1` to right should trigger `kKnock` effect.
ScrollToSwitchDesks(/*scroll_left=*/false,
/*event_generator=*/GetEventGenerator());
EXPECT_EQ(1, desk_controller->GetActiveDeskIndex());
EXPECT_EQ(1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock,
HapticTouchpadEffectStrength::kMedium));
}
// Tests that haptics are sent when doing a continuous touchpad gesture to
// switch desks. They are expected to be sent if we hit the edge, or when the
// visible desk changes.
TEST_F(HapticsUtilTest, HapticFeedbackForContinuousDesksSwitching) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
// Add three desks for a total of four.
auto* desks_controller = DesksController::Get();
desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
// Create a standalone animation object. This is the same object that gets
// created when swiping with 4 fingers, but mocking 4 fingers swipes is harder
// to control in a test with all the async operations and touchpad unit
// conversions.
DeskActivationAnimation animation(desks_controller, 0, 1,
DesksSwitchSource::kDeskSwitchTouchpad,
/*update_window_activation=*/false);
animation.set_skip_notify_controller_on_animation_finished_for_testing(true);
animation.Launch();
// Wait for the ending screenshot to be taken.
WaitUntilEndingScreenshotTaken(&animation);
EXPECT_EQ(0, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock,
HapticTouchpadEffectStrength::kMedium));
EXPECT_EQ(0, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kTick,
HapticTouchpadEffectStrength::kMedium));
// Swipe enough so that our third and fourth desk screenshots are taken, and
// then swipe so that the fourth desk is fully shown. There should be 3
// visible desk changes in total, which means 3 tick haptic events sent.
animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
WaitUntilEndingScreenshotTaken(&animation);
animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
WaitUntilEndingScreenshotTaken(&animation);
animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
EXPECT_EQ(3, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kTick,
HapticTouchpadEffectStrength::kMedium));
// Try doing a full swipe to the right. Test that a knock haptic event is sent
// because we are at the edge.
animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
EXPECT_EQ(1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock,
HapticTouchpadEffectStrength::kMedium));
// Swipe 3 times to the left. We move from the fourth desk as the visible desk
// to the first desk, so there should be three more tick haptic events.
animation.UpdateSwipeAnimation(kTouchpadSwipeLengthForDeskChange);
animation.UpdateSwipeAnimation(kTouchpadSwipeLengthForDeskChange);
animation.UpdateSwipeAnimation(kTouchpadSwipeLengthForDeskChange);
EXPECT_EQ(6, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kTick,
HapticTouchpadEffectStrength::kMedium));
// Swipe to the left while at the first desk. Tests that another haptic event
// is sent because we are at the edge.
animation.UpdateSwipeAnimation(kTouchpadSwipeLengthForDeskChange);
EXPECT_EQ(2, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kKnock,
HapticTouchpadEffectStrength::kMedium));
}
// Tests that haptics are sent when dragging a window/desk in overview.
TEST_F(HapticsUtilTest, HapticFeedbackForDragAndDrop) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
OverviewController* overview_controller = Shell::Get()->overview_controller();
std::unique_ptr<aura::Window> window = CreateTestWindow();
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Add three desks for a total of two.
auto* desks_controller = DesksController::Get();
desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
// Drag a window in overview. Test that kTick feedback is sent.
EnterOverview();
auto* overview_item =
overview_controller->overview_session()->GetOverviewItemForWindow(
window.get());
const gfx::RectF bounds_f = overview_item->target_bounds();
event_generator->set_current_screen_location(
gfx::ToRoundedPoint(bounds_f.CenterPoint()));
event_generator->PressLeftButton();
event_generator->MoveMouseTo(gfx::ToRoundedPoint(bounds_f.right_center()));
EXPECT_EQ(1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kTick,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseLeftButton();
EXPECT_TRUE(overview_controller->InOverviewSession());
// Drag a desk in overview. Test that kTick feedback is sent.
const gfx::Rect bounds = overview_controller->overview_session()
->grid_list()
.front()
->desks_bar_view()
->mini_views()
.front()
->bounds();
event_generator->set_current_screen_location(bounds.CenterPoint());
event_generator->PressLeftButton();
event_generator->MoveMouseTo(bounds.right_center());
EXPECT_EQ(2, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kTick,
HapticTouchpadEffectStrength::kMedium));
event_generator->ReleaseLeftButton();
EXPECT_TRUE(overview_controller->InOverviewSession());
ExitOverview();
}
TEST_F(HapticsUtilTest, HapticFeedbackForMultitaskMenu) {
auto input_controller =
std::make_unique<HapticsTrackingTestInputController>();
std::unique_ptr<aura::Window> window = CreateAppWindow();
// Show the clamshell multitask menu via hover. Test that kSnap feedback is
// sent.
chromeos::MultitaskMenu* multitask_menu = ShowAndWaitMultitaskMenuForWindow(
window.get(), chromeos::MultitaskMenuEntryType::kFrameSizeButtonHover);
ASSERT_TRUE(multitask_menu);
EXPECT_EQ(1, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
// Test that the kSnap feedback is sent whenever we hover over one of the
// multitask menu buttons.
auto* event_generator = GetEventGenerator();
chromeos::MultitaskMenuViewTestApi test_api(
multitask_menu->multitask_menu_view());
event_generator->MoveMouseTo(
test_api.GetFloatButton()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(2, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->MoveMouseTo(
test_api.GetFullButton()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(3, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
chromeos::SplitButtonView* half_button = test_api.GetHalfButton();
event_generator->MoveMouseTo(
half_button->GetLeftTopButton()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(4, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
event_generator->MoveMouseTo(
half_button->GetRightBottomButton()->GetBoundsInScreen().CenterPoint());
EXPECT_EQ(5, input_controller->GetSentHapticCount(
HapticTouchpadEffect::kSnap,
HapticTouchpadEffectStrength::kMedium));
}
} // namespace ash