blob: 8f80a6ce411fa56a9fd04a03ccf0cbdee08283bf [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_internal.h"
#include <compare>
#include <deque>
#include <memory>
#include <set>
#include <utility>
#include <variant>
#include <vector>
#include "base/containers/map_util.h"
#include "base/strings/strcat.h"
#include "build/build_config.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/framework_specific_implementation.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/interaction/interactive_test_internal.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/native_window_tracker/native_window_tracker.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interaction_test_util_mouse.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/interaction/widget_focus_observer.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ui/aura/test/aura_test_helper.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "ui/base/interaction/interaction_test_util_mac.h"
#endif
namespace views::test::internal {
namespace {
// Basic observer for low-level activation changes. Relays when a widget
// receives focus.
class NativeViewWidgetFocusSupplier : public WidgetFocusSupplier {
public:
NativeViewWidgetFocusSupplier() : observer_(test::AnyWidgetTestPasskey{}) {
observer_.set_activated_callback(
base::BindRepeating(&NativeViewWidgetFocusSupplier::OnWidgetActivated,
base::Unretained(this)));
}
~NativeViewWidgetFocusSupplier() override = default;
DECLARE_FRAMEWORK_SPECIFIC_METADATA()
void OnWidgetActivated(Widget* widget) {
// OnAnyWidgetActivated is only called for activation, so we don't have to
// worry about spurious nullptrs from deactivation.
OnWidgetFocusChanged(widget);
}
protected:
Widget::Widgets GetAllWidgets() const override {
return WidgetTest::GetAllWidgets();
}
private:
AnyWidgetObserver observer_;
};
DEFINE_FRAMEWORK_SPECIFIC_METADATA(NativeViewWidgetFocusSupplier)
// Takes a list of tracked `views` and massages them into a tree based on the
// views hierarchy, with widgets at the top level. (Widget parenting may be
// handled in a later update).
//
// Only views on the list are returned, which means that many container views
// may be omitted.
//
// Assumptions:
// - All `TrackedElementViews` correspond to views in the view hierarchy, and
// are attached to widgets.
// - There are no circular parent-child relations in the views hierarchy.
InteractiveViewsTestPrivate::DebugTreeNodeViews::List DebugDumpViewHierarchy(
std::vector<const TrackedElementViews*> views) {
using Node = InteractiveViewsTestPrivate::DebugTreeNodeViews;
using List = InteractiveViewsTestPrivate::DebugTreeNodeViews::List;
// Need to know what views are participating in the hierarchy.
std::map<const View*, const TrackedElementViews*> known_views;
for (const auto& view : views) {
known_views.emplace(view->view(), view);
}
// Keep separate track of widget nodes, which are fixed, and view nodes,
// which can be in various places.
std::map<const Widget*, Node> widget_nodes;
std::map<const View*, Node*> view_nodes;
for (const auto& view : views) {
// It's possible this view was already added as the ancestor of another.
if (view_nodes.contains(view->view())) {
continue;
}
// Ensure a widget node exists.
const Widget* const widget = view->view()->GetWidget();
if (!widget_nodes.contains(widget)) {
widget_nodes.emplace(widget, Node(widget));
}
// Add all known views in the current view's hierarchy, if they are not
// already present.
//
// This ensure that upstream nodes are always created first, and nodes never
// have to be moved after they are created.
std::deque<const TrackedElementViews*> views_to_add{view};
Node* to_add_to = nullptr;
// Walk up the view hierarchy from the current view.
for (const View* ancestor = view->view()->parent(); ancestor != nullptr;
ancestor = ancestor->parent()) {
// If there is already a node for an ancestor, add to that node.
const auto it = view_nodes.find(ancestor);
if (it != view_nodes.end()) {
to_add_to = it->second;
break;
}
// If there is no node but this is a known element, it must be added.
if (known_views.contains(ancestor)) {
views_to_add.push_front(known_views[ancestor]);
}
}
// If no view in the hierarchy already has a node, add to the widget's node
// instead.
if (!to_add_to) {
to_add_to = &widget_nodes[widget];
}
// Walk down the view hierarchy adding nodes from ancestor to descendant.
for (const auto* view_to_add : views_to_add) {
CHECK(view_to_add);
CHECK(view_to_add->view());
const auto add_child_result =
to_add_to->children.emplace(view_to_add->view(), view_to_add);
CHECK(add_child_result.second);
// This is safe since we just verified we inserted the value.
Node* const ptr = const_cast<Node*>(&*add_child_result.first);
const auto add_entry_result =
view_nodes.emplace(view_to_add->view(), ptr);
CHECK(add_entry_result.second);
to_add_to = add_entry_result.first->second;
}
}
// Only widget nodes are included in the final list, since all views should
// belong to a widget.
List top_level;
for (auto& entry : widget_nodes) {
top_level.emplace(std::move(entry.second));
}
return top_level;
}
} // namespace
DEFINE_FRAMEWORK_SPECIFIC_METADATA(InteractiveViewsTestPrivate)
InteractiveViewsTestPrivate::DebugTreeNodeViews::DebugTreeNodeViews() = default;
InteractiveViewsTestPrivate::DebugTreeNodeViews::DebugTreeNodeViews(
const View* view,
const ui::TrackedElement* el)
: impl(view), element(el), bounds(view->GetBoundsInScreen()) {}
InteractiveViewsTestPrivate::DebugTreeNodeViews::DebugTreeNodeViews(
const Widget* widget)
: impl(widget), bounds(widget->GetWindowBoundsInScreen()) {}
InteractiveViewsTestPrivate::DebugTreeNodeViews::DebugTreeNodeViews(
DebugTreeNodeViews&&) noexcept = default;
InteractiveViewsTestPrivate::DebugTreeNodeViews&
InteractiveViewsTestPrivate::DebugTreeNodeViews::operator=(
DebugTreeNodeViews&&) noexcept = default;
InteractiveViewsTestPrivate::DebugTreeNodeViews::~DebugTreeNodeViews() =
default;
InteractiveViewsTestPrivate::DebugTreeNode
InteractiveViewsTestPrivate::DebugTreeNodeViews::ToNode(
const InteractiveViewsTestPrivate& owner) const {
InteractiveViewsTestPrivate::DebugTreeNode result;
if (std::holds_alternative<const View*>(impl)) {
result = *owner.DebugDumpElement(element);
} else {
result =
DebugTreeNode(owner.DebugDumpWidget(*std::get<const Widget*>(impl)));
}
for (auto& child : children) {
result.children.emplace_back(child.ToNode(owner));
}
return result;
}
std::strong_ordering
InteractiveViewsTestPrivate::DebugTreeNodeViews::operator<=>(
const DebugTreeNodeViews& other) const {
auto result = bounds.x() <=> other.bounds.x();
if (result != std::strong_ordering::equal) {
return result;
}
result = bounds.y() <=> other.bounds.y();
if (result != std::strong_ordering::equal) {
return result;
}
return impl <=> other.impl;
}
// Caches the last-known native window associated with a context.
// Useful for executing ClickMouse() and ReleaseMouse() commands, as no target
// element is provided for those commands. A NativeWindowTracker is used to
// prevent using a cached value after the native window has been destroyed.
class InteractiveViewsTestPrivate::WindowHintCacheEntry {
public:
WindowHintCacheEntry() = default;
~WindowHintCacheEntry() = default;
WindowHintCacheEntry(WindowHintCacheEntry&& other) = default;
WindowHintCacheEntry& operator=(WindowHintCacheEntry&& other) = default;
bool IsValid() const {
return window_ && tracker_ && !tracker_->WasNativeWindowDestroyed();
}
gfx::NativeWindow GetWindow() const {
return IsValid() ? window_ : gfx::NativeWindow();
}
void SetWindow(gfx::NativeWindow window) {
if (window_ == window) {
return;
}
window_ = window;
tracker_ = window ? ui::NativeWindowTracker::Create(window) : nullptr;
}
private:
gfx::NativeWindow window_ = gfx::NativeWindow();
std::unique_ptr<ui::NativeWindowTracker> tracker_;
};
InteractiveViewsTestPrivate::InteractiveViewsTestPrivate(
ui::test::internal::InteractiveTestPrivate& test_impl)
: ui::test::internal::InteractiveTestPrivateFrameworkBase(test_impl) {
test_impl.test_util().AddSimulator(
std::make_unique<views::test::InteractionTestUtilSimulatorViews>());
#if BUILDFLAG(IS_MAC)
test_impl.test_util().AddSimulator(
std::make_unique<ui::test::InteractionTestUtilSimulatorMac>());
#endif
}
InteractiveViewsTestPrivate::~InteractiveViewsTestPrivate() = default;
void InteractiveViewsTestPrivate::OnSequenceComplete() {
if (mouse_util_) {
mouse_util_->CancelAllGestures();
}
}
void InteractiveViewsTestPrivate::OnSequenceAborted(
const ui::InteractionSequence::AbortedData& data) {
if (mouse_util_) {
mouse_util_->CancelAllGestures();
}
}
void InteractiveViewsTestPrivate::DoTestSetUp() {
// Frame should exist from set up to tear down, to prevent framework/system
// listeners from receiving events outside of the test.
widget_focus_supplier_frame_ = std::make_unique<WidgetFocusSupplierFrame>();
widget_focus_suppliers().MaybeRegister<NativeViewWidgetFocusSupplier>();
}
void InteractiveViewsTestPrivate::DoTestTearDown() {
// Avoid doing any widget focus tracking after the test completes.
widget_focus_supplier_frame_.reset();
}
InteractionTestUtilMouse::GestureParams
InteractiveViewsTestPrivate::GetGestureParamsForStep(
ui::TrackedElement* el,
const ui::InteractionSequence* seq) {
// Get the native window.
gfx::NativeWindow window = test_impl().GetNativeWindowFor(el);
// If a window was found, then a cache entry may need to be inserted/updated.
if (window) {
// This is just a find if the entry already exists.
auto result =
window_hint_cache_.try_emplace(el->context(), WindowHintCacheEntry());
// This is a no-op if this is already the cached window.
result.first->second.SetWindow(window);
}
return InteractionTestUtilMouse::GestureParams(
window, seq->IsCurrentStepImmediateForTesting());
}
gfx::NativeWindow InteractiveViewsTestPrivate::GetNativeWindowFromElement(
const ui::TrackedElement* el) const {
gfx::NativeWindow window = gfx::NativeWindow();
if (el->IsA<TrackedElementViews>()) {
// Most widgets have an associated native window.
const Widget* const widget =
el->AsA<TrackedElementViews>()->view()->GetWidget();
window = widget->GetNativeWindow();
// Most of those that don't are sub-widgets that are hard-parented to
// another widget.
if (!window && widget->parent()) {
window = widget->parent()->GetNativeWindow();
}
// At worst case, fall back to the primary window.
if (!window) {
window = widget->GetPrimaryWindowWidget()->GetNativeWindow();
}
}
return window;
}
gfx::NativeWindow InteractiveViewsTestPrivate::GetNativeWindowFromContext(
ui::ElementContext context) const {
// Used the cached value, if one exists.
const auto it = window_hint_cache_.find(context);
return it != window_hint_cache_.end() ? it->second.GetWindow()
: gfx::NativeWindow();
}
std::string InteractiveViewsTestPrivate::DebugDumpWidget(
const Widget& widget) const {
std::string description = widget.GetName();
return base::StrCat({// At any time, at most one widget can be active. It is
// the widget that accepts keyboard inputs.
widget.IsActive() ? "[ACTIVE] " : "",
widget.GetClassName(), " \"", widget.GetName(), "\" at ",
DebugDumpBounds(widget.GetWindowBoundsInScreen())});
}
std::vector<InteractiveViewsTestPrivate::DebugTreeNode>
InteractiveViewsTestPrivate::DebugDumpElements(
std::set<const ui::TrackedElement*>& elements) const {
std::vector<const TrackedElementViews*> views;
for (auto it = elements.begin(); it != elements.end();) {
if (const auto* const view_el = (*it)->AsA<TrackedElementViews>()) {
views.push_back(view_el);
it = elements.erase(it);
} else {
++it;
}
}
std::vector<InteractiveViewsTestPrivate::DebugTreeNode> result;
for (auto& view_node : DebugDumpViewHierarchy(views)) {
result.emplace_back(view_node.ToNode(*this));
}
return result;
}
std::optional<InteractiveViewsTestPrivate::DebugTreeNode>
InteractiveViewsTestPrivate::DebugDumpElement(
const ui::TrackedElement* el) const {
if (const auto* view = el->AsA<TrackedElementViews>()) {
return DebugTreeNode(base::StrCat(
{(view->view()->HasFocus() ? "[FOCUSED] " : ""),
view->view()->GetClassName(), " - ", el->identifier().GetName(),
" at ", DebugDumpBounds(el->GetScreenBounds())}));
}
return std::nullopt;
}
} // namespace views::test::internal