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/base/interaction/interactive_test_internal.h"

#include <iterator>
#include <memory>
#include <ostream>
#include <sstream>
#include <string_view>
#include <variant>
#include <vector>

#include "base/callback_list.h"
#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_test_util.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/framework_specific_implementation.h"
#include "ui/gfx/native_ui_types.h"

namespace ui::test::internal {

namespace {

// Basic implementation of the framework for dumping generic elements.
class InteractiveTestPrivateFrameworkImpl
    : public InteractiveTestPrivateFrameworkBase {
 public:
  DECLARE_FRAMEWORK_SPECIFIC_METADATA()

  explicit InteractiveTestPrivateFrameworkImpl(
      InteractiveTestPrivate& test_impl)
      : InteractiveTestPrivateFrameworkBase(test_impl) {}
  ~InteractiveTestPrivateFrameworkImpl() override = default;

  std::vector<DebugTreeNode> DebugDumpElements(
      std::set<const ui::TrackedElement*>& elements) const override {
    std::vector<DebugTreeNode> nodes;
    for (auto* el : elements) {
      if (el->identifier() == kInteractiveTestPivotElementId) {
        nodes.insert(nodes.begin(),
                     DebugTreeNode("Pivot element (part of test automation)"));
      } else {
        nodes.emplace_back(
            base::StringPrintf("%s - %s at %s", el->GetImplementationName(),
                               el->identifier().GetName().c_str(),
                               DebugDumpBounds(el->GetScreenBounds())));
      }
    }
    elements.clear();
    return nodes;
  }

  std::string DebugDescribeContext(ui::ElementContext context) const override {
    std::ostringstream oss;
    oss << context;
    return oss.str();
  }
};

DEFINE_FRAMEWORK_SPECIFIC_METADATA(InteractiveTestPrivateFrameworkImpl)

}  // namespace

DEFINE_ELEMENT_IDENTIFIER_VALUE(kInteractiveTestPivotElementId);
DEFINE_CUSTOM_ELEMENT_EVENT_TYPE(kInteractiveTestPivotEventType);
DEFINE_STATE_IDENTIFIER_VALUE(PollingStateObserver<bool>,
                              kInteractiveTestPollUntilState);

StateObserverElement::StateObserverElement(ElementIdentifier id,
                                           ElementContext context)
    : TestElementBase(id, context) {}

StateObserverElement::~StateObserverElement() = default;

DEFINE_FRAMEWORK_SPECIFIC_METADATA(StateObserverElement)

// static
bool InteractiveTestPrivate::allow_interactive_test_verbs_ = false;

InteractiveTestPrivate::AdditionalContext::AdditionalContext() = default;

InteractiveTestPrivate::AdditionalContext::AdditionalContext(
    InteractiveTestPrivate& owner,
    intptr_t handle)
    : owner_(owner.GetAsWeakPtr()), handle_(handle) {
  CHECK(handle);
}

InteractiveTestPrivate::AdditionalContext::AdditionalContext(
    const AdditionalContext& other) = default;

InteractiveTestPrivate::AdditionalContext&
InteractiveTestPrivate::AdditionalContext::operator=(
    const AdditionalContext& other) = default;

InteractiveTestPrivate::AdditionalContext::~AdditionalContext() = default;

void InteractiveTestPrivate::AdditionalContext::Set(
    const std::string_view& additional_context) {
  auto* const owner = owner_.get();
  CHECK(owner) << "Set() should never be executed after destruction of the "
                  "owning sequence.";
  CHECK(handle_)
      << "Set() should never be executed on a default-constructed object.";
  owner_->additional_context_data_[handle_] = additional_context;
}

std::string InteractiveTestPrivate::AdditionalContext::Get() const {
  auto* const owner = owner_.get();
  CHECK(owner) << "Set() should never be executed after destruction of the "
                  "owning sequence.";
  CHECK(handle_)
      << "Set() should never be executed on a default-constructed object.";
  const auto it = owner_->additional_context_data_.find(handle_);
  return (it != owner_->additional_context_data_.end()) ? it->second
                                                        : std::string();
}

void InteractiveTestPrivate::AdditionalContext::Clear() {
  auto* const owner = owner_.get();
  CHECK(owner) << "Clear() should never be executed after destruction of the "
                  "owning sequence.";
  CHECK(handle_)
      << "Clear() should never be executed on a default-constructed object.";
  owner_->additional_context_data_.erase(handle_);
}

InteractiveTestPrivate::InteractiveTestPrivate() {
  MaybeRegisterFrameworkImpl<InteractiveTestPrivateFrameworkImpl>();
}

InteractiveTestPrivate::~InteractiveTestPrivate() = default;

void InteractiveTestPrivate::Init(ElementContext initial_context) {
  success_ = false;
  sequence_skipped_ = false;
  MaybeAddPivotElement(initial_context);
  for (ElementContext context :
       ElementTracker::GetElementTracker()->GetAllContextsForTesting()) {
    MaybeAddPivotElement(context);
  }
  context_subscription_ =
      ElementTracker::GetElementTracker()->AddAnyElementShownCallbackForTesting(
          base::BindRepeating(&InteractiveTestPrivate::OnElementAdded,
                              base::Unretained(this)));
}

void InteractiveTestPrivate::Cleanup() {
  context_subscription_ = base::CallbackListSubscription();
  pivot_elements_.clear();
}

void InteractiveTestPrivate::OnElementAdded(TrackedElement* el) {
  if (el->identifier() == kInteractiveTestPivotElementId)
    return;
  MaybeAddPivotElement(el->context());
}

void InteractiveTestPrivate::MaybeAddPivotElement(ElementContext context) {
  CHECK(context) << "Attempted to run steps in an invalid (null) context.";
  if (!base::Contains(pivot_elements_, context)) {
    auto pivot =
        std::make_unique<TestElement>(kInteractiveTestPivotElementId, context);
    auto* const el = pivot.get();
    pivot_elements_.emplace(context, std::move(pivot));
    el->Show();
  }
}

base::WeakPtr<InteractiveTestPrivate> InteractiveTestPrivate::GetAsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

gfx::NativeWindow InteractiveTestPrivate::GetNativeWindowFor(
    const ui::TrackedElement* el) const {
  for (auto& framework : framework_implementations_) {
    if (auto result = framework.GetNativeWindowFromElement(el)) {
      return result;
    }
  }
  for (auto& framework : framework_implementations_) {
    if (auto result = framework.GetNativeWindowFromContext(el->context())) {
      return result;
    }
  }
  return gfx::NativeWindow();
}

void InteractiveTestPrivate::HandleActionResult(
    InteractionSequence* seq,
    const TrackedElement* el,
    const std::string& operation_name,
    ActionResult result) {
  switch (result) {
    case ActionResult::kSucceeded:
      break;
    case ActionResult::kFailed:
      LOG(ERROR) << operation_name << " failed for " << *el;
      seq->FailForTesting();
      break;
    case ActionResult::kNotAttempted:
      LOG(ERROR) << operation_name << " could not be applied to " << *el;
      seq->FailForTesting();
      break;
    case ActionResult::kKnownIncompatible:
      LOG(WARNING) << operation_name
                   << " failed because it is unsupported on this platform for "
                   << *el;
      if (!on_incompatible_action_reason_.empty()) {
        LOG(WARNING) << "Unsupported action was expected: "
                     << on_incompatible_action_reason_;
      } else {
        LOG(ERROR) << "Unsupported action was unexpected. "
                      "Did you forget to call SetOnIncompatibleAction()?";
      }
      switch (on_incompatible_action_) {
        case OnIncompatibleAction::kFailTest:
          seq->FailForTesting();
          break;
        case OnIncompatibleAction::kSkipTest:
        case OnIncompatibleAction::kHaltTest:
          sequence_skipped_ = true;
          seq->FailForTesting();
          break;
        case OnIncompatibleAction::kIgnoreAndContinue:
          break;
      }
      break;
  }
}

TrackedElement* InteractiveTestPrivate::GetPivotElement(
    ElementContext context) const {
  const auto it = pivot_elements_.find(context);
  CHECK(it != pivot_elements_.end())
      << "Tried to reference non-existent context.";
  return it->second.get();
}

bool InteractiveTestPrivate::RemoveStateObserver(ElementIdentifier id,
                                                 ElementContext context) {
  using It = decltype(state_observer_elements_.begin());
  It found = state_observer_elements_.end();
  for (It it = state_observer_elements_.begin();
       it != state_observer_elements_.end(); ++it) {
    auto& entry = **it;
    if (entry.identifier() == id && (!context || entry.context() == context)) {
      CHECK(found == state_observer_elements_.end())
          << "RemoveStateObserver: Duplicate entries found for " << id;
      found = it;
    }
  }
  if (found == state_observer_elements_.end()) {
    LOG(ERROR) << "RemoveStateObserver: Entry not found for " << id;
    return false;
  }

  state_observer_elements_.erase(found);
  return true;
}

InteractiveTestPrivate::AdditionalContext
InteractiveTestPrivate::CreateAdditionalContext() {
  return AdditionalContext(*this, next_additional_context_handle_++);
}

std::vector<std::string> InteractiveTestPrivate::GetAdditionalContext() const {
  std::vector<std::string> entries;
  std::transform(additional_context_data_.begin(),
                 additional_context_data_.end(), std::back_inserter(entries),
                 [](const auto& entry) { return entry.second; });
  return entries;
}

void InteractiveTestPrivate::DoTestSetUp() {
  for (auto& framework : framework_implementations_) {
    framework.DoTestSetUp();
  }
}
void InteractiveTestPrivate::DoTestTearDown() {
  for (auto& framework : base::Reversed(framework_implementations_)) {
    framework.DoTestTearDown();
  }
  state_observer_elements_.clear();
}

void InteractiveTestPrivate::OnSequenceComplete() {
  for (auto& framework : base::Reversed(framework_implementations_)) {
    framework.OnSequenceComplete();
  }
  success_ = true;
}

void InteractiveTestPrivate::OnSequenceAborted(
    const InteractionSequence::AbortedData& data) {
  for (auto& framework : base::Reversed(framework_implementations_)) {
    framework.OnSequenceAborted(data);
  }
  if (aborted_callback_for_testing_) {
    std::move(aborted_callback_for_testing_).Run(data);
    return;
  }
  if (sequence_skipped_) {
    LOG(WARNING) << kInteractiveTestFailedMessagePrefix << data;
    if (on_incompatible_action_ == OnIncompatibleAction::kSkipTest) {
      GTEST_SKIP();
    } else {
      DCHECK_EQ(OnIncompatibleAction::kHaltTest, on_incompatible_action_);
    }
  } else {
    std::ostringstream additional_message;
    if (data.aborted_reason == InteractionSequence::AbortedReason::
                                   kElementHiddenBetweenTriggerAndStepStart) {
      additional_message
          << "\nNOTE: Please check for one of the following common mistakes:\n"
             " - A RunLoop whose type is not set to kNestableTasksAllowed. "
             "Change the type and try again.\n"
             " - A check being performed on an element that has been hidden. "
             "Wrap waiting for the hide and subsequent checks in a "
             "WithoutDelay() to avoid possible access-after-delete.";
    }
    const auto additional_context = GetAdditionalContext();
    if (!additional_context.empty()) {
      additional_message << "\nAdditional test context:";
      for (const auto& ctx : additional_context) {
        additional_message << "\n * " << ctx;
      }
    }
    DebugDumpElements(data.context).PrintTo(additional_message);
    GTEST_FAIL() << "Interactive test failed " << data
                 << additional_message.str();
  }
}

InteractiveTestPrivateFrameworkBase::InteractiveTestPrivateFrameworkBase(
    InteractiveTestPrivate& test_impl)
    : test_impl_(test_impl) {}
InteractiveTestPrivateFrameworkBase::~InteractiveTestPrivateFrameworkBase() =
    default;
InteractiveTestPrivateFrameworkBase::DebugTreeNode::DebugTreeNode() = default;
InteractiveTestPrivateFrameworkBase::DebugTreeNode::DebugTreeNode(
    std::string initial_text)
    : text(initial_text) {}
InteractiveTestPrivateFrameworkBase::DebugTreeNode::DebugTreeNode(
    DebugTreeNode&&) noexcept = default;
InteractiveTestPrivateFrameworkBase::DebugTreeNode&
InteractiveTestPrivateFrameworkBase::DebugTreeNode::operator=(
    DebugTreeNode&&) noexcept = default;
InteractiveTestPrivateFrameworkBase::DebugTreeNode::~DebugTreeNode() = default;

std::vector<InteractiveTestPrivateFrameworkBase::DebugTreeNode>
InteractiveTestPrivateFrameworkBase::DebugDumpElements(
    std::set<const ui::TrackedElement*>& el) const {
  return {};
}
std::string InteractiveTestPrivateFrameworkBase::DebugDescribeContext(
    ui::ElementContext context) const {
  return std::string();
}

// static
std::string InteractiveTestPrivateFrameworkBase::DebugDumpBounds(
    const gfx::Rect& bounds) {
  return base::StringPrintf("x:%d-%d y:%d-%d (%dx%d)", bounds.x(),
                            bounds.right(), bounds.y(), bounds.bottom(),
                            bounds.width(), bounds.height());
}

gfx::NativeWindow
InteractiveTestPrivateFrameworkBase::GetNativeWindowFromElement(
    const TrackedElement* el) const {
  // Default answer is "no window is associated with this element".
  // Other framework implementations will provide ways to convert specific
  // types of elements to their native windows.
  return gfx::NativeWindow();
}

gfx::NativeWindow
InteractiveTestPrivateFrameworkBase::GetNativeWindowFromContext(
    ElementContext) const {
  // Default answer is "no window is associated with this context".
  // Other framework implementations will provide ways to convert specific
  // contexts to their native windows.
  return gfx::NativeWindow();
}

namespace {
void PrintDebugTree(std::ostream& stream,
                    const InteractiveTestPrivate::DebugTreeNode& node,
                    std::string prefix,
                    bool last) {
  stream << prefix;
  if (prefix.empty()) {
    stream << "\n";
    prefix += "  ";
  } else {
    if (last) {
      stream << "╰─";
      prefix += "   ";
    } else {
      stream << "├─";
      prefix += "│  ";
    }
  }
  stream << node.text << '\n';
  for (size_t i = 0; i < node.children.size(); ++i) {
    const bool last_child = (i == node.children.size() - 1);
    PrintDebugTree(stream, node.children[i], prefix, last_child);
  }
}
}  // namespace

void InteractiveTestPrivate::DebugTreeNode::PrintTo(
    std::ostream& stream) const {
  PrintDebugTree(stream, *this, "", true);
}

InteractiveTestPrivate::DebugTreeNode InteractiveTestPrivate::DebugDumpElements(
    ui::ElementContext current_context) const {
  DebugTreeNode node("UI Elements");
  const auto* const tracker = ui::ElementTracker::GetElementTracker();
  for (const auto ctx : tracker->GetAllContextsForTesting()) {
    DebugTreeNode ctx_node = DebugDumpContext(ctx);
    if (ctx == current_context) {
      ctx_node.text = "[CURRENT CONTEXT] " + ctx_node.text;
    }
    node.children.emplace_back(std::move(ctx_node));
  }
  return node;
}

InteractiveTestPrivate::DebugTreeNode InteractiveTestPrivate::DebugDumpContext(
    ui::ElementContext context) const {
  std::string context_description;
  for (auto& framework_implementation :
       base::Reversed(framework_implementations_)) {
    context_description =
        framework_implementation.DebugDescribeContext(context);
    if (!context_description.empty()) {
      break;
    }
  }
  CHECK(!context_description.empty());
  DebugTreeNode node(context_description);
  auto element_list =
      ui::ElementTracker::GetElementTracker()->GetAllElementsForTesting(
          context);
  std::set<const ui::TrackedElement*> elements(element_list.begin(),
                                               element_list.end());
  std::vector<std::vector<DebugTreeNode>> temp;
  for (auto& framework_implementation :
       base::Reversed(framework_implementations_)) {
    temp.push_back(framework_implementation.DebugDumpElements(elements));
  }
  CHECK(elements.empty());
  for (auto& nodes : base::Reversed(temp)) {
    std::move(nodes.begin(), nodes.end(), std::back_inserter(node.children));
  }
  return node;
}
std::string DescribeElement(ElementSpecifier element) {
  std::ostringstream oss;
  oss << element;
  return oss.str();
}

InteractionSequence::Builder BuildSubsequence(
    InteractiveTestPrivate::MultiStep steps) {
  InteractionSequence::Builder builder;
  for (auto& step : steps) {
    builder.AddStep(std::move(step));
  }
  return builder;
}

}  // namespace ui::test::internal