blob: bb3aa8710cc7f6c57e94351ebbba18a6b9906b69 [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/interactive_views_test.h"
#include <functional>
#include <optional>
#include <string_view>
#include <variant>
#include "base/functional/callback_forward.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "build/build_config.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/test/ui_controls.h"
#include "ui/views/interaction/interaction_test_util_mouse.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/interaction/interactive_views_test_internal.h"
#include "ui/views/view_tracker.h"
#if BUILDFLAG(IS_MAC)
#include "ui/base/interaction/interaction_test_util_mac.h"
#endif
namespace views::test {
using GestureParams = InteractionTestUtilMouse::GestureParams;
using ui::test::internal::kInteractiveTestPivotElementId;
InteractiveViewsTestApi::InteractiveViewsTestApi()
: test_impl_(private_test_impl()
.MaybeRegisterFrameworkImpl<
internal::InteractiveViewsTestPrivate>()) {}
InteractiveViewsTestApi::~InteractiveViewsTestApi() = default;
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::NameView(
std::string_view name,
AbsoluteViewSpecifier spec) {
return NameViewRelative(kInteractiveTestPivotElementId, name,
GetFindViewCallback(std::move(spec)));
}
// static
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::NameChildView(
ElementSpecifier parent,
std::string_view name,
ChildViewSpecifier spec) {
return std::move(
NameViewRelative(parent, name, GetFindViewCallback(std::move(spec)))
.SetDescription(
base::StringPrintf("NameChildView( \"%s\" )", name.data())));
}
// static
ui::InteractionSequence::StepBuilder
InteractiveViewsTestApi::NameDescendantView(ElementSpecifier parent,
std::string_view name,
ViewMatcher matcher) {
return std::move(
NameViewRelative(
parent, name,
base::BindOnce(
[](ViewMatcher matcher, View* ancestor) -> View* {
auto* const result =
FindMatchingView(ancestor, matcher, /* recursive =*/true);
if (!result) {
LOG(ERROR)
<< "NameDescendantView(): No descendant matches matcher.";
}
return result;
},
matcher))
.SetDescription(
base::StringPrintf("NameDescendantView( \"%s\" )", name.data())));
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::ScrollIntoView(
ElementSpecifier view) {
return std::move(WithView(view, [](View* v) {
v->ScrollViewToVisible();
}).SetDescription("ScrollIntoView()"));
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::MoveMouseTo(
ElementSpecifier reference,
RelativePositionSpecifier position) {
RequireInteractiveTest();
StepBuilder step;
step.SetDescription("MoveMouseTo()");
step.SetElement(reference);
step.SetStartCallback(base::BindOnce(
[](InteractiveViewsTestApi* test, RelativePositionCallback pos_callback,
ui::InteractionSequence* seq, ui::TrackedElement* el) {
test->test_impl_->mouse_error_message_.clear();
const auto weak_seq = seq->AsWeakPtr();
if (!test->mouse_util().PerformGestures(
test->test_impl_->GetGestureParamsForStep(el, seq),
InteractionTestUtilMouse::MoveTo(
std::move(pos_callback).Run(el)))) {
if (weak_seq) {
weak_seq->FailForTesting();
}
}
},
base::Unretained(this), GetPositionCallback(std::move(position))));
return step;
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::MoveMouseTo(
AbsolutePositionSpecifier position) {
return MoveMouseTo(kInteractiveTestPivotElementId,
GetPositionCallback(std::move(position)));
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::ClickMouse(
ui_controls::MouseButton button,
bool release,
int modifier_keys) {
RequireInteractiveTest();
StepBuilder step;
step.SetDescription("ClickMouse()");
step.SetElementID(kInteractiveTestPivotElementId);
step.SetStartCallback(base::BindOnce(
[](InteractiveViewsTestApi* test, ui_controls::MouseButton button,
bool release, int modifier_keys, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
test->test_impl_->mouse_error_message_.clear();
const auto weak_seq = seq->AsWeakPtr();
if (!test->mouse_util().PerformGestures(
test->test_impl_->GetGestureParamsForStep(el, seq),
release ? InteractionTestUtilMouse::Click(button, modifier_keys)
: InteractionTestUtilMouse::MouseGestures{
InteractionTestUtilMouse::MouseDown(
button, modifier_keys)})) {
if (weak_seq) {
weak_seq->FailForTesting();
}
}
},
base::Unretained(this), button, release, modifier_keys));
step.SetMustRemainVisible(false);
return step;
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::DragMouseTo(
ElementSpecifier reference,
RelativePositionSpecifier position,
bool release) {
RequireInteractiveTest();
StepBuilder step;
step.SetDescription("DragMouseTo()");
step.SetElement(reference);
step.SetStartCallback(base::BindOnce(
[](InteractiveViewsTestApi* test, RelativePositionCallback pos_callback,
bool release, ui::InteractionSequence* seq, ui::TrackedElement* el) {
test->test_impl_->mouse_error_message_.clear();
const gfx::Point target = std::move(pos_callback).Run(el);
const auto weak_seq = seq->AsWeakPtr();
if (!test->mouse_util().PerformGestures(
test->test_impl_->GetGestureParamsForStep(el, seq),
release ? InteractionTestUtilMouse::DragAndRelease(target)
: InteractionTestUtilMouse::DragAndHold(target))) {
if (weak_seq) {
weak_seq->FailForTesting();
}
}
},
base::Unretained(this), GetPositionCallback(std::move(position)),
release));
return step;
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::DragMouseTo(
AbsolutePositionSpecifier position,
bool release) {
return DragMouseTo(kInteractiveTestPivotElementId,
GetPositionCallback(std::move(position)), release);
}
InteractiveViewsTestApi::StepBuilder InteractiveViewsTestApi::ReleaseMouse(
ui_controls::MouseButton button,
int modifier_keys) {
RequireInteractiveTest();
StepBuilder step;
step.SetDescription("ReleaseMouse()");
step.SetElementID(kInteractiveTestPivotElementId);
step.SetStartCallback(base::BindOnce(
[](InteractiveViewsTestApi* test, ui_controls::MouseButton button,
int modifier_keys, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
test->test_impl_->mouse_error_message_.clear();
const auto weak_seq = seq->AsWeakPtr();
if (!test->mouse_util().PerformGestures(
test->test_impl_->GetGestureParamsForStep(el, seq),
InteractionTestUtilMouse::MouseUp(button, modifier_keys))) {
if (weak_seq) {
weak_seq->FailForTesting();
}
}
},
base::Unretained(this), button, modifier_keys));
step.SetMustRemainVisible(false);
return step;
}
// static
InteractiveViewsTestApi::FindViewCallback
InteractiveViewsTestApi::GetFindViewCallback(AbsoluteViewSpecifier spec) {
return std::visit(
absl::Overload{
[](View* view) {
CHECK(view) << "NameView(View*): view must be set.";
return base::BindOnce(
[](const std::unique_ptr<ViewTracker>& ref, View*) {
LOG_IF(ERROR, !ref->view())
<< "NameView(View*): view ceased to be "
"valid before step was executed.";
return ref->view();
},
std::make_unique<ViewTracker>(view));
},
[](std::reference_wrapper<View*> ref) {
return base::BindOnce(
[](std::reference_wrapper<View*> ref, View*) {
LOG_IF(ERROR, !ref.get())
<< "NameView(View*): view ceased to be "
"valid before step was executed.";
return ref.get();
},
ref);
},
[](base::OnceCallback<View*()>& callback) {
return base::RectifyCallback<FindViewCallback>(std::move(callback));
}},
spec);
}
// static
InteractiveViewsTestApi::FindViewCallback
InteractiveViewsTestApi::GetFindViewCallback(ChildViewSpecifier spec) {
return std::visit(
absl::Overload{
[](size_t index) {
return base::BindOnce(
[](size_t index, View* parent) -> View* {
if (index >= parent->children().size()) {
LOG(ERROR)
<< "NameChildView(int): Child index out of bounds; got "
<< index << " but only " << parent->children().size()
<< " children.";
return nullptr;
}
return parent->children()[index];
},
index);
},
[](ViewMatcher& matcher) {
return base::BindOnce(
[](ViewMatcher matcher, View* parent) -> View* {
auto* const result =
FindMatchingView(parent, matcher, /*recursive =*/false);
LOG_IF(ERROR, !result) << "NameChildView(ViewMatcher): No "
"child matches matcher.";
return result;
},
std::move(matcher));
}},
spec);
}
// static
View* InteractiveViewsTestApi::FindMatchingView(const View* from,
ViewMatcher& matcher,
bool recursive) {
for (views::View* const child : from->children()) {
if (matcher.Run(child)) {
return child;
}
if (recursive) {
auto* const result = FindMatchingView(child, matcher, true);
if (result) {
return result;
}
}
}
return nullptr;
}
void InteractiveViewsTestApi::SetContextWidget(Widget* widget) {
context_widget_ = widget ? widget->GetWeakPtr() : nullptr;
if (widget) {
private_test_impl().set_default_context(
ElementTrackerViews::GetContextForWidget(widget));
CHECK(!test_impl_->mouse_util_)
<< "Changing the context widget during a test is not supported.";
test_impl_->mouse_util_ =
std::make_unique<InteractionTestUtilMouse>(widget);
} else {
private_test_impl().set_default_context(ui::ElementContext());
test_impl_->mouse_util_.reset();
}
}
// static
InteractiveViewsTestApi::RelativePositionCallback
InteractiveViewsTestApi::GetPositionCallback(AbsolutePositionSpecifier spec) {
return std::visit(
absl::Overload{
[](const gfx::Point& point) {
return base::BindOnce(
[](gfx::Point p, ui::TrackedElement*) { return p; }, point);
},
[](std::reference_wrapper<gfx::Point> point) {
return base::BindOnce([](std::reference_wrapper<gfx::Point> p,
ui::TrackedElement*) { return p.get(); },
point);
},
[](AbsolutePositionCallback& callback) {
return base::RectifyCallback<RelativePositionCallback>(
std::move(callback));
}},
spec);
}
// static
InteractiveViewsTestApi::RelativePositionCallback
InteractiveViewsTestApi::GetPositionCallback(RelativePositionSpecifier spec) {
return std::visit(
absl::Overload{[](RelativePositionCallback& callback) {
return std::move(callback);
},
[](CenterPoint) {
return base::BindOnce([](ui::TrackedElement* el) {
CHECK(el->IsA<views::TrackedElementViews>());
return el->AsA<views::TrackedElementViews>()
->view()
->GetBoundsInScreen()
.CenterPoint();
});
}},
spec);
}
} // namespace views::test