blob: e234e2b72201ca20a1ca55296c48e04ca8517636 [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/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