910e62b5创建于 1月15日历史提交
// 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