blob: 56ed6c45265df1e28a30b30353af91036791ddc0 [file] [log] [blame]
// 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 "ui/views/interaction/interaction_test_util_mouse.h"
#include <memory>
#include <utility>
#include <variant>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "ui/base/test/ui_controls.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/widget/widget.h"
#if defined(USE_AURA)
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/drag_drop_client_observer.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#endif // defined(USE_AURA)
// Currently, touch is only supported on ChromeOS Ash.
#if BUILDFLAG(IS_CHROMEOS)
#define TOUCH_INPUT_SUPPORTED 1
#else
#define TOUCH_INPUT_SUPPORTED 0
#endif
namespace views::test {
namespace {
raw_ptr<InteractionTestUtilMouse> g_current_mouse_util = nullptr;
void PostTask(base::OnceClosure task) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(task));
}
#if TOUCH_INPUT_SUPPORTED
ui_controls::TouchType GetTouchAction(ui_controls::MouseButtonState state) {
switch (state) {
case ui_controls::DOWN:
return ui_controls::kTouchPress;
case ui_controls::UP:
return ui_controls::kTouchRelease;
}
}
int GetTouchCount(ui_controls::MouseButton button) {
switch (button) {
case ui_controls::LEFT:
return 1;
case ui_controls::MIDDLE:
return 3;
case ui_controls::RIGHT:
return 2;
}
}
#endif // TOUCH_INPUT_SUPPORTED
} // namespace
InteractionTestUtilMouse::GestureParams::GestureParams() = default;
InteractionTestUtilMouse::GestureParams::GestureParams(
gfx::NativeWindow window_hint_,
bool force_async_)
: window_hint(window_hint_), force_async(force_async_) {}
InteractionTestUtilMouse::GestureParams::GestureParams(const GestureParams&) =
default;
InteractionTestUtilMouse::GestureParams&
InteractionTestUtilMouse::GestureParams::operator=(const GestureParams&) =
default;
InteractionTestUtilMouse::GestureParams::GestureParams(
GestureParams&&) noexcept = default;
InteractionTestUtilMouse::GestureParams&
InteractionTestUtilMouse::GestureParams::operator=(GestureParams&&) noexcept =
default;
InteractionTestUtilMouse::GestureParams::~GestureParams() = default;
#if defined(USE_AURA)
// Ends any drag currently in progress or that starts during this object's
// lifetime. This is needed because the drag controller can get out of sync with
// mouse event handling - especially when running ChromeOS-on-Linux. This can
// result in weird test hangs/timeouts during mouse-up after a drag, or (more
// insidiously) during test shutdown.
//
// Once started, the DragEnder will kill any drags that start until:
// - Stop() is called.
// - The Aura window it is watching goes away.
// - The DragEnder is destroyed (which should happen no earlier than the end of
// ShutDownOnMainThread()).
class InteractionTestUtilMouse::DragEnder
: public aura::client::DragDropClientObserver,
public aura::WindowObserver {
public:
explicit DragEnder(aura::Window* window) : window_(window) {
window_observation_.Observe(window);
}
~DragEnder() override = default;
// Either cancels a current drag, or starts observing for a future drag start
// event (at which point the drag will be canceled).
void Start() {
if (CancelDragNow() || drag_client_observation_.IsObserving()) {
return;
}
// Only Ash actually supports observing the drag-drop client. Therefore, on
// other platforms, only direct cancel is possible.
#if BUILDFLAG(IS_CHROMEOS)
if (auto* const client = GetClient()) {
drag_client_observation_.Observe(client);
}
#endif
}
// Stops any ongoing observation of drag start events.
void Stop() { drag_client_observation_.Reset(); }
// Cancels any drag that is currently happening, but does not watch for future
// drag start events.
bool CancelDragNow() {
if (auto* const client = GetClient()) {
if (client->IsDragDropInProgress()) {
LOG(WARNING)
<< "InteractionTestUtilMouse: Force-canceling spurious drag "
"operation.\n"
<< "This can happen when the drag controller gets out of sync with "
"mouse events being sent by the test, and is especially likely "
"on ChromeOS-on-Linux.\n"
<< "This is not necessarily a serious error if the test functions "
"normally; however, if you see this too often or your test "
"flakes as a result of the cancel you may need to restructure "
"the test so that you can be sure the drag has started before "
"attempting to invoke ReleaseMouse().";
client->DragCancel();
return true;
}
}
return false;
}
base::WeakPtr<DragEnder> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
private:
// aura::client::DragDropClientObserver:
void OnDragStarted() override {
drag_client_observation_.Reset();
PostTask(base::BindOnce(base::IgnoreResult(&DragEnder::CancelDragNow),
weak_ptr_factory_.GetWeakPtr()));
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
DCHECK_EQ(window_, window);
drag_client_observation_.Reset();
window_observation_.Reset();
window_ = nullptr;
}
aura::client::DragDropClient* GetClient() {
return window_ ? aura::client::GetDragDropClient(window_->GetRootWindow())
: nullptr;
}
// Since there is no "DragDropClientDestroying" event, use the aura::Window as
// a proxy for the existence of the DragDropClient, and unregister listeners
// when the window goes away. If this is not done, UAFs may happen when the
// scoped observation of the drag client goes away.
raw_ptr<aura::Window> window_;
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
base::ScopedObservation<aura::client::DragDropClient,
aura::client::DragDropClientObserver>
drag_client_observation_{this};
base::WeakPtrFactory<DragEnder> weak_ptr_factory_{this};
};
#endif // defined(USE_AURA)
InteractionTestUtilMouse::InteractionTestUtilMouse(views::Widget* widget)
: InteractionTestUtilMouse(widget->GetNativeWindow()) {}
InteractionTestUtilMouse::~InteractionTestUtilMouse() {
CHECK(!performing_gestures_)
<< "InteractionTestUtilMouse destroyed with pending actions.";
LOG_IF(ERROR, g_current_mouse_util != this)
<< "Expected |this| to be current InteractionTestUtilMouse.";
g_current_mouse_util = nullptr;
}
// static
InteractionTestUtilMouse::MouseGesture InteractionTestUtilMouse::MoveTo(
gfx::Point point) {
return MouseGesture(point);
}
// static
InteractionTestUtilMouse::MouseGesture InteractionTestUtilMouse::MouseDown(
ui_controls::MouseButton button,
int modifier_keys) {
return MouseButtonGesture(button, ui_controls::DOWN, modifier_keys);
}
// static
InteractionTestUtilMouse::MouseGesture InteractionTestUtilMouse::MouseUp(
ui_controls::MouseButton button,
int modifier_keys) {
return MouseButtonGesture(button, ui_controls::UP, modifier_keys);
}
// static
InteractionTestUtilMouse::MouseGestures InteractionTestUtilMouse::Click(
ui_controls::MouseButton button,
int modifier_keys) {
return MouseGestures{MouseDown(button, modifier_keys),
MouseUp(button, modifier_keys)};
}
// static
InteractionTestUtilMouse::MouseGestures InteractionTestUtilMouse::DragAndHold(
gfx::Point destination) {
return MouseGestures{MouseDown(ui_controls::LEFT), MoveTo(destination)};
}
// static
InteractionTestUtilMouse::MouseGestures
InteractionTestUtilMouse::DragAndRelease(gfx::Point destination) {
return MouseGestures{MouseDown(ui_controls::LEFT), MoveTo(destination),
MouseUp(ui_controls::LEFT)};
}
bool InteractionTestUtilMouse::ShouldCancelDrag() const {
#if defined(USE_AURA)
return dragging_;
#else
return false;
#endif
}
void InteractionTestUtilMouse::CancelFutureDrag() {
#if defined(USE_AURA)
// Allow the system to finish processing any mouse input before force-
// canceling any ongoing drag. It's possible that a drag that was queued to
// complete simply hasn't yet.
PostTask(base::BindOnce(&DragEnder::Start, drag_ender_->GetWeakPtr()));
#endif
}
void InteractionTestUtilMouse::CancelDragNow() {
#if defined(USE_AURA)
CHECK(!dragging_);
drag_ender_->Stop();
drag_ender_->CancelDragNow();
#endif
}
bool InteractionTestUtilMouse::SendButtonPress(
const MouseButtonGesture& gesture,
const GestureParams& params,
base::OnceClosure on_complete) {
if (!params.force_async) {
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_) {
return ui_controls::SendTouchEventsNotifyWhenDone(
GetTouchAction(gesture.button_state), GetTouchCount(gesture.button),
touch_hover_point_.x(), touch_hover_point_.y(),
std::move(on_complete));
}
#endif // TOUCH_INPUT_SUPPORTED
return ui_controls::SendMouseEventsNotifyWhenDone(
gesture.button, gesture.button_state, std::move(on_complete),
gesture.modifier_keys, params.window_hint);
}
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_) {
PostTask(base::BindOnce(
[](base::WeakPtr<InteractionTestUtilMouse> util,
base::OnceClosure on_complete, MouseButtonGesture gesture,
gfx::Point target) {
if (!util) {
return;
}
CHECK(ui_controls::SendTouchEventsNotifyWhenDone(
GetTouchAction(gesture.button_state),
GetTouchCount(gesture.button), target.x(), target.y(),
std::move(on_complete)));
},
weak_ptr_factory_.GetWeakPtr(), std::move(on_complete), gesture,
touch_hover_point_));
return true;
}
#endif // TOUCH_INPUT_SUPPORTED
PostTask(base::BindOnce(
[](base::WeakPtr<InteractionTestUtilMouse> util,
base::OnceClosure on_complete, MouseButtonGesture gesture,
gfx::NativeWindow window_hint) {
if (!util) {
return;
}
CHECK(ui_controls::SendMouseEventsNotifyWhenDone(
gesture.button, gesture.button_state, std::move(on_complete),
gesture.modifier_keys, window_hint));
},
weak_ptr_factory_.GetWeakPtr(), std::move(on_complete), gesture,
params.window_hint));
return true;
}
bool InteractionTestUtilMouse::SendMove(const MouseMoveGesture& gesture,
const GestureParams& params,
base::OnceClosure on_complete) {
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_) {
// Need to remember where our finger is.
touch_hover_point_ = gesture;
// If no fingers are down, there's nothing to do.
if (buttons_down_.empty()) {
std::move(on_complete).Run();
return true;
}
// Should never have two different sets of fingers down at once.
CHECK_EQ(1U, buttons_down_.size());
}
#endif // TOUCH_INPUT_SUPPORTED
if (!params.force_async) {
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_) {
return ui_controls::SendTouchEventsNotifyWhenDone(
ui_controls::kTouchMove, GetTouchCount(*buttons_down_.begin()),
gesture.x(), gesture.y(), std::move(on_complete));
}
#endif // TOUCH_INPUT_SUPPORTED
return ui_controls::SendMouseMoveNotifyWhenDone(
gesture.x(), gesture.y(), std::move(on_complete), params.window_hint);
}
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_) {
PostTask(base::BindOnce(
[](base::WeakPtr<InteractionTestUtilMouse> util,
base::OnceClosure on_complete, MouseMoveGesture gesture) {
if (!util) {
return;
}
const int touch_count = GetTouchCount(*util->buttons_down_.begin());
CHECK(ui_controls::SendTouchEventsNotifyWhenDone(
ui_controls::kTouchMove, touch_count, gesture.x(), gesture.y(),
std::move(on_complete)));
},
weak_ptr_factory_.GetWeakPtr(), std::move(on_complete), gesture));
return true;
}
#endif // TOUCH_INPUT_SUPPORTED
PostTask(base::BindOnce(
[](base::WeakPtr<InteractionTestUtilMouse> util,
base::OnceClosure on_complete, MouseMoveGesture gesture,
gfx::NativeWindow window_hint) {
if (!util) {
return;
}
CHECK(ui_controls::SendMouseMoveNotifyWhenDone(
gesture.x(), gesture.y(), std::move(on_complete), window_hint));
},
weak_ptr_factory_.GetWeakPtr(), std::move(on_complete), gesture,
params.window_hint));
return true;
}
bool InteractionTestUtilMouse::SetTouchMode(bool touch_mode) {
if (touch_mode == touch_mode_) {
return true;
}
CHECK(buttons_down_.empty())
<< "Cannot toggle touch mode when buttons or fingers are down.";
#if TOUCH_INPUT_SUPPORTED
touch_mode_ = touch_mode;
return true;
#else
LOG(WARNING) << "Touch mode not supported on this platform.";
return false;
#endif
}
bool InteractionTestUtilMouse::GetTouchMode() const {
return touch_mode_;
}
bool InteractionTestUtilMouse::PerformGesturesImpl(const GestureParams& params,
MouseGestures gestures) {
CHECK(!gestures.empty());
CHECK(!performing_gestures_);
base::AutoReset<bool> performing_gestures(&performing_gestures_, true);
canceled_ = false;
for (auto& gesture : gestures) {
if (canceled_) {
break;
}
base::RunLoop run_loop{base::RunLoop::Type::kNestableTasksAllowed};
if (MouseButtonGesture* const button =
std::get_if<MouseButtonGesture>(&gesture)) {
switch (button->button_state) {
case ui_controls::UP: {
CHECK(buttons_down_.erase(button->button));
base::OnceClosure on_complete =
params.force_async ? base::DoNothing() : run_loop.QuitClosure();
if (ShouldCancelDrag()) {
// This will bail out of any nested drag-drop run loop, allowing
// the code to proceed even if the drag somehow starts while the
// mouse-up is being processed.
on_complete = std::move(on_complete)
.Then(base::BindOnce(
&InteractionTestUtilMouse::CancelFutureDrag,
weak_ptr_factory_.GetWeakPtr()));
}
#if defined(USE_AURA)
dragging_ = false;
#endif
if (!SendButtonPress(*button, params, std::move(on_complete))) {
LOG(ERROR) << "Mouse button " << button->button << " up failed.";
return false;
}
if (!params.force_async) {
run_loop.Run();
}
break;
}
case ui_controls::DOWN:
#if TOUCH_INPUT_SUPPORTED
CHECK(!touch_mode_ || buttons_down_.empty())
<< "In touch mode, only one set of fingers may be down at any "
"given time.";
#endif
CHECK(buttons_down_.insert(button->button).second);
#if BUILDFLAG(IS_MAC)
if (!params.force_async && button->button == ui_controls::RIGHT) {
LOG(WARNING)
<< "InteractionTestUtilMouse::PerformGestures() - "
"Important note:\n"
<< "Because right-clicking on Mac typically results in a "
"context menu, and because some (but not all) context menus "
"on Mac are native and take over the main message loop, "
"right-clicking could cause the test to hang.\n"
<< "If you notice your test hangs on Mac, use "
"MayInvolveNativeContextMenu() and minimize the number of "
"test "
"steps performed while the context menu is open.";
}
#endif
CancelDragNow();
if (!SendButtonPress(*button, params,
params.force_async ? base::DoNothing()
: run_loop.QuitClosure())) {
LOG(ERROR) << "Mouse button " << button->button << " down failed.";
return false;
}
if (!params.force_async) {
run_loop.Run();
}
break;
}
} else {
const auto& move = std::get<MouseMoveGesture>(gesture);
#if defined(USE_AURA)
if (!buttons_down_.empty()) {
CHECK(base::Contains(buttons_down_, ui_controls::LEFT));
dragging_ = true;
}
#endif
if (!SendMove(move, params,
params.force_async ? base::DoNothing()
: run_loop.QuitClosure())) {
LOG(ERROR) << "Mouse move to " << move.ToString() << " failed.";
return false;
}
if (!params.force_async) {
run_loop.Run();
}
}
}
return !canceled_;
}
void InteractionTestUtilMouse::CancelAllGestures() {
weak_ptr_factory_.InvalidateWeakPtrs();
canceled_ = true;
#if TOUCH_INPUT_SUPPORTED
if (touch_mode_ && !buttons_down_.empty()) {
// Should never get in a state where multiple finger combinations are down
// at the same time.
CHECK_EQ(1U, buttons_down_.size());
const auto& button = *buttons_down_.begin();
if (!ui_controls::SendTouchEvents(
ui_controls::kTouchRelease, GetTouchCount(button),
touch_hover_point_.x(), touch_hover_point_.y())) {
LOG(WARNING) << "Unable to send touch up.";
}
buttons_down_.clear();
}
#endif // TOUCH_INPUT_SUPPORTED
// Now that no additional actions will happen, release all mouse buttons.
for (ui_controls::MouseButton button : buttons_down_) {
if (!ui_controls::SendMouseEvents(button, ui_controls::UP)) {
LOG(WARNING) << "Unable to release mouse button " << button;
}
}
buttons_down_.clear();
// Maybe handle dragging stopped.
if (ShouldCancelDrag()) {
CancelFutureDrag();
}
}
InteractionTestUtilMouse::InteractionTestUtilMouse(gfx::NativeWindow window)
#if defined(USE_AURA)
: drag_ender_(std::make_unique<DragEnder>(window))
#endif
{
CHECK(window);
CHECK(!g_current_mouse_util)
<< "Cannot have multiple overlapping InteractionTestUtilMouse instances";
g_current_mouse_util = this;
}
// static
void InteractionTestUtilMouse::AddGestures(MouseGestures& gestures,
MouseGesture to_add) {
gestures.emplace_back(std::move(to_add));
}
// static
void InteractionTestUtilMouse::AddGestures(MouseGestures& gestures,
MouseGestures to_add) {
for (auto& gesture : to_add) {
gestures.emplace_back(std::move(gesture));
}
}
} // namespace views::test