blob: c7ba4b5ebc70128f164bb7fb5706a3c23ee548c2 [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 "chrome/test/interaction/interactive_browser_test_internal.h"
#include <compare>
#include <functional>
#include <memory>
#include <sstream>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/devtools_agent_coverage_observer.h"
#include "chrome/test/base/test_switches.h"
#include "chrome/test/interaction/interaction_test_util_browser.h"
#include "chrome/test/interaction/tracked_element_webcontents.h"
#include "content/public/browser/web_contents.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/gfx/native_widget_types.h"
#include "ui/views/interaction/interactive_views_test_internal.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/shell.h"
#include "ui/aura/window.h"
#endif
namespace internal {
InteractiveBrowserTestPrivate::InteractiveBrowserTestPrivate(
std::unique_ptr<InteractionTestUtilBrowser> test_util)
: InteractiveViewsTestPrivate(std::move(test_util)) {}
InteractiveBrowserTestPrivate::~InteractiveBrowserTestPrivate() = default;
void InteractiveBrowserTestPrivate::DoTestTearDown() {
// Release any remaining instrumented WebContents.
instrumented_web_contents_.clear();
// If the test has elected to engage WebUI code coverage, write out the
// resulting data.
if (coverage_observer_) {
const auto* const test_info =
testing::UnitTest::GetInstance()->current_test_info();
std::string test_name =
base::StrCat({test_info->test_suite_name(), ".", test_info->name()});
// Parameterized tests tend to have slashes in them, which can interfere
// with file system paths. Change them to something else.
std::replace(test_name.begin(), test_name.end(), '/', '_');
LOG(INFO) << "Writing out WebUI code coverage data. If this causes the "
"test to time out (b/273290598), you may want to disable "
"coverage until the performance can be improved. If the "
"test crashes, a page touched by the test is likely still "
"incompatible with coverage (see b/273545898).";
coverage_observer_->CollectCoverage(test_name);
}
InteractiveViewsTestPrivate::DoTestTearDown();
}
void InteractiveBrowserTestPrivate::MaybeStartWebUICodeCoverage() {
if (coverage_observer_) {
return;
}
base::CommandLine* const command_line =
base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kDevtoolsCodeCoverage)) {
base::FilePath devtools_code_coverage_dir =
command_line->GetSwitchValuePath(switches::kDevtoolsCodeCoverage);
coverage_observer_ = std::make_unique<DevToolsAgentCoverageObserver>(
devtools_code_coverage_dir);
LOG(WARNING) << "Starting WebUI code coverage. This may cause the test to "
"take longer, possibly resulting in timeouts. Also, due to "
"issues with the coverage logic, some WebUI pages may not "
"be compatible with WebUI code coverage.";
}
}
void InteractiveBrowserTestPrivate::AddInstrumentedWebContents(
std::unique_ptr<WebContentsInteractionTestUtil> instrumented_web_contents) {
for (const auto& existing : instrumented_web_contents_) {
CHECK_NE(instrumented_web_contents->page_identifier(),
existing->page_identifier());
}
instrumented_web_contents_.emplace_back(std::move(instrumented_web_contents))
.get();
}
bool InteractiveBrowserTestPrivate::IsInstrumentedWebContents(
ui::ElementIdentifier element_id) const {
for (const auto& existing : instrumented_web_contents_) {
if (existing->page_identifier() == element_id) {
return true;
}
}
return false;
}
bool InteractiveBrowserTestPrivate::UninstrumentWebContents(
ui::ElementIdentifier to_remove) {
for (auto it = instrumented_web_contents_.begin();
it != instrumented_web_contents_.end(); ++it) {
if ((*it)->page_identifier() == to_remove) {
instrumented_web_contents_.erase(it);
return true;
}
}
return false;
}
std::string InteractiveBrowserTestPrivate::DeepQueryToString(
const WebContentsInteractionTestUtil::DeepQuery& deep_query) {
std::ostringstream oss;
oss << "{";
for (size_t i = 0; i < deep_query.size(); ++i) {
if (i) {
oss << ", ";
}
oss << "\"" << deep_query[i] << "\"";
}
oss << "}";
return oss.str();
}
gfx::NativeWindow InteractiveBrowserTestPrivate::GetNativeWindowFromElement(
ui::TrackedElement* el) const {
gfx::NativeWindow window = gfx::NativeWindow();
// For instrumented WebContents, we can get the native window directly from
// the contents object.
if (el->IsA<TrackedElementWebContents>()) {
auto* const util = el->AsA<TrackedElementWebContents>()->owner();
window = util->web_contents()->GetTopLevelNativeWindow();
}
// If that did not work, fall back to the base implementation.
if (!window)
window = InteractiveViewsTestPrivate::GetNativeWindowFromElement(el);
return window;
}
gfx::NativeWindow InteractiveBrowserTestPrivate::GetNativeWindowFromContext(
ui::ElementContext context) const {
// Defer to the base implementation first, since there may be a cached value
// that is more accurate than what can be inferred from the context.
gfx::NativeWindow window =
InteractiveViewsTestPrivate::GetNativeWindowFromContext(context);
// If that didn't work, fall back to the top-level browser window for the
// context (assuming there is one).
if (!window) {
if (Browser* const browser =
InteractionTestUtilBrowser::GetBrowserFromContext(context)) {
if (BrowserView* const browser_view =
BrowserView::GetBrowserViewForBrowser(browser)) {
window = browser_view->GetNativeWindow();
}
}
}
return window;
}
std::string InteractiveBrowserTestPrivate::DebugDescribeContext(
ui::ElementContext context) const {
if (const auto* browser =
InteractionTestUtilBrowser::GetBrowserFromContext(context)) {
std::string type;
switch (browser->type()) {
case Browser::TYPE_APP:
type = "App window";
break;
case Browser::TYPE_APP_POPUP:
type = "Popup app window";
break;
case Browser::TYPE_NORMAL:
type = "Tabbed browser window";
break;
case Browser::TYPE_DEVTOOLS:
type = "Devtools window";
break;
case Browser::TYPE_PICTURE_IN_PICTURE:
type = "Picture-in-picture window";
break;
default:
type = "Other browser window";
break;
}
if (browser->SupportsWindowFeature(Browser::FEATURE_TABSTRIP)) {
type += base::StringPrintf(", %d tab(s) (active: %d)",
browser->tab_strip_model()->count(),
browser->tab_strip_model()->active_index());
}
return base::StringPrintf(
"%s%s profile %s%s at %s",
(browser->window()->IsActive() ? "[ACTIVE] " : ""), type,
browser->profile()->GetDebugName(),
(browser->profile()->IsOffTheRecord() ? " (off-the-record)" : ""),
DebugDumpBounds(browser->window()->GetBounds()));
} else {
return InteractiveViewsTestPrivate::DebugDescribeContext(context);
}
}
InteractiveBrowserTestPrivate::DebugTreeNode
InteractiveBrowserTestPrivate::DebugDumpElement(
const ui::TrackedElement* el) const {
if (const auto* contents = el->AsA<TrackedElementWebContents>()) {
auto* const web_contents = contents->owner()->web_contents();
int index = TabStripModel::kNoTab;
if (const auto* browser =
InteractionTestUtilBrowser::GetBrowserFromContext(el->context())) {
index = browser->tab_strip_model()->GetIndexOfWebContents(web_contents);
}
return DebugTreeNode(base::StringPrintf(
"WebContents %s - %s at %s with URL \"%s\"",
(index == TabStripModel::kNoTab
? "in secondary UI"
: base::StringPrintf("in tab %d", index).c_str()),
el->identifier().GetName(), DebugDumpBounds(el->GetScreenBounds()),
web_contents->GetURL().spec().c_str()));
} else {
return InteractiveViewsTestPrivate::DebugDumpElement(el);
}
}
MatchableValue::MatchableValue() noexcept = default;
MatchableValue::MatchableValue(const base::Value& value) noexcept
: value_(value.Clone()) {}
MatchableValue::MatchableValue(base::Value&& value) noexcept
: value_(std::move(value)) {}
MatchableValue::MatchableValue(const MatchableValue& value) noexcept
: value_(value.value_.Clone()) {}
MatchableValue::MatchableValue(MatchableValue&&) noexcept = default;
MatchableValue& MatchableValue::operator=(const base::Value& value) noexcept {
value_ = value.Clone();
return *this;
}
MatchableValue& MatchableValue::operator=(base::Value&& value) noexcept {
value_ = std::move(value);
return *this;
}
MatchableValue& MatchableValue::operator=(
const MatchableValue& value) noexcept {
if (this != &value) {
value_ = value.value_.Clone();
}
return *this;
}
MatchableValue& MatchableValue::operator=(MatchableValue&&) noexcept = default;
MatchableValue::~MatchableValue() = default;
void CheckValueTypes(const MatchableValue& source,
const MatchableValue& target) {
using Type = base::Value::Type;
const auto source_type = source.value().type();
const auto target_type = target.value().type();
if (target_type == Type::DOUBLE &&
(source_type == Type::DOUBLE || source_type == Type::INTEGER)) {
// This is an allowed conversion.
return;
}
// Explicitly don't allow downcast to integer for comparison.
if (target_type == Type::INTEGER) {
CHECK_NE(source_type, Type::DOUBLE)
<< "JS returned a floating-point value (" << source
<< ") but comparison was with an integer (" << target
<< "). If there is any chance the value will be floating-point, "
"compare to a double value instead.";
}
// Otherwise, the types *must* match.
CHECK_EQ(source_type, target_type) << "Type mismatch attempting to compare "
<< source << " (from JS) and " << target;
}
bool MatchableValue::operator==(const MatchableValue& other) const {
CheckValueTypes(*this, other);
if (other.value_.type() == base::Value::Type::DOUBLE) {
return value_.GetDouble() == other.value_.GetDouble();
}
return value_ == other.value_;
}
namespace {
template <template <typename...> class Op>
bool MatchableValueCompare(const MatchableValue& lhs,
const MatchableValue& rhs) {
CheckValueTypes(lhs, rhs);
switch (rhs.value().type()) {
case base::Value::Type::DOUBLE:
return Op<double>()(lhs.value().GetDouble(), rhs.value().GetDouble());
case base::Value::Type::INTEGER:
return Op<double>()(lhs.value().GetInt(), rhs.value().GetInt());
case base::Value::Type::STRING:
return Op<std::string>()(lhs.value().GetString(),
rhs.value().GetString());
default:
NOTREACHED() << "Target value " << rhs << " (" << rhs.value().type()
<< ") does not support greater than/less than comparison.";
}
}
} // namespace
MatchableValue::operator std::string() const {
return value_.GetString();
}
bool MatchableValue::operator<(const MatchableValue& other) const {
return MatchableValueCompare<std::less>(*this, other);
}
bool MatchableValue::operator>(const MatchableValue& other) const {
return MatchableValueCompare<std::greater>(*this, other);
}
bool MatchableValue::operator<=(const MatchableValue& other) const {
return MatchableValueCompare<std::less_equal>(*this, other);
}
bool MatchableValue::operator>=(const MatchableValue& other) const {
return MatchableValueCompare<std::greater_equal>(*this, other);
}
std::ostream& operator<<(std::ostream& out, const MatchableValue& value) {
return out << value.value();
}
bool IsTruthyMatcher::MatchAndExplain(
const internal::MatchableValue& x,
testing::MatchResultListener* listener) const {
return WebContentsInteractionTestUtil::IsTruthy(x.value());
}
void IsTruthyMatcher::DescribeTo(std::ostream* os) const {
*os << "is truthy";
}
void IsTruthyMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "is falsy";
}
} // namespace internal