blob: f9299e230e9d2f3f5f58a16ccdb97f29214e6bf7 [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.h"
#include <ostream>
#include <sstream>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/interaction/interaction_test_util_browser.h"
#include "chrome/test/interaction/interactive_browser_test_internal.h"
#include "chrome/test/interaction/tracked_element_webcontents.h"
#include "chrome/test/interaction/webcontents_interaction_test_util.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/update_user_activation_state_interceptor.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-shared.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom-shared.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.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/views/controls/webview/webview.h"
#include "ui/views/interaction/interactive_views_test.h"
#include "ui/views/views_delegate.h"
namespace {
// Since we enforce a 1:1 correspondence between ElementIdentifiers and
// WebContents defaulting to ContextMode::kAny prevents accidentally missing the
// correct context, which is a common mistake that causes tests to mysteriously
// time out looking in the wrong place.
constexpr ui::InteractionSequence::ContextMode kDefaultWebContentsContextMode =
ui::InteractionSequence::ContextMode::kAny;
// Matcher that determines whether a particular value is truthy.
class IsTruthyMatcher : public testing::MatcherInterface<const base::Value&> {
public:
using is_gtest_matcher = void;
bool MatchAndExplain(const base::Value& x,
testing::MatchResultListener* listener) const override {
return WebContentsInteractionTestUtil::IsTruthy(x);
}
void DescribeTo(std::ostream* os) const override { *os << "is truthy"; }
void DescribeNegationTo(std::ostream* os) const override {
*os << "is falsy";
}
};
} // namespace
InteractiveBrowserTestApi::InteractiveBrowserTestApi()
: InteractiveBrowserTestApi(
std::make_unique<internal::InteractiveBrowserTestPrivate>(
std::make_unique<InteractionTestUtilBrowser>())) {}
InteractiveBrowserTestApi::InteractiveBrowserTestApi(
std::unique_ptr<internal::InteractiveBrowserTestPrivate> private_test_impl)
: InteractiveViewsTestApi(std::move(private_test_impl)) {}
InteractiveBrowserTestApi::~InteractiveBrowserTestApi() = default;
// static
WebContentsInteractionTestUtil*
InteractiveBrowserTestApi::AsInstrumentedWebContents(ui::TrackedElement* el) {
auto* const web_el = el->AsA<TrackedElementWebContents>();
CHECK(web_el);
return web_el->owner();
}
void InteractiveBrowserTestApi::EnableWebUICodeCoverage() {
test_impl().MaybeStartWebUICodeCoverage();
}
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::Screenshot(
ElementSpecifier element,
const std::string& screenshot_name,
const std::string& baseline) {
StepBuilder builder;
builder.SetDescription(base::StringPrintf("Screenshot( \"%s\", \"%s\" )",
screenshot_name.c_str(),
baseline.c_str()));
ui::test::internal::SpecifyElement(builder, element);
builder.SetStartCallback(base::BindOnce(
[](InteractiveBrowserTestApi* test, std::string screenshot_name,
std::string baseline, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
const auto result = InteractionTestUtilBrowser::CompareScreenshot(
el, screenshot_name, baseline);
test->test_impl().HandleActionResult(seq, el, "Screenshot", result);
},
base::Unretained(this), screenshot_name, baseline));
return builder;
}
InteractiveBrowserTestApi::MultiStep InteractiveBrowserTestApi::InstrumentTab(
ui::ElementIdentifier id,
std::optional<int> tab_index,
BrowserSpecifier in_browser,
bool wait_for_ready) {
const auto desc =
base::StringPrintf("InstrumentTab( %s, %d, %d )", id.GetName().c_str(),
tab_index.value_or(-1), wait_for_ready);
auto steps = Steps(std::move(
WithElement(ui::test::internal::kInteractiveTestPivotElementId,
base::BindLambdaForTesting([this, id, tab_index, in_browser](
ui::TrackedElement* el) {
Browser* const browser =
GetBrowserFor(el->context(), in_browser);
CHECK(browser)
<< "InstrumentTab(): a specific browser is required.";
test_impl().AddInstrumentedWebContents(
WebContentsInteractionTestUtil::ForExistingTabInBrowser(
browser, id, tab_index));
}))
.SetDescription(base::StrCat({desc, ": Instrument"}))));
if (wait_for_ready) {
steps.emplace_back(std::move(WaitForWebContentsReady(id).FormatDescription(
base::StrCat({desc, ": %s"}))));
}
return steps;
}
ui::InteractionSequence::StepBuilder
InteractiveBrowserTestApi::InstrumentNextTab(ui::ElementIdentifier id,
BrowserSpecifier in_browser) {
return std::move(
WithElement(
ui::test::internal::kInteractiveTestPivotElementId,
base::BindLambdaForTesting([this, id,
in_browser](ui::TrackedElement* el) {
Browser* const browser = GetBrowserFor(el->context(), in_browser);
test_impl().AddInstrumentedWebContents(
browser
? WebContentsInteractionTestUtil::ForNextTabInBrowser(
browser, id)
: WebContentsInteractionTestUtil::ForNextTabInAnyBrowser(
id));
}))
.SetDescription(
base::StringPrintf("InstrumentTab( %s )", id.GetName().c_str())));
}
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::AddInstrumentedTab(ui::ElementIdentifier id,
GURL url,
std::optional<int> at_index,
BrowserSpecifier in_browser) {
const auto desc = base::StringPrintf("AddInstrumentedTab( %s, %s, %d, )",
id.GetName().c_str(), url.spec().c_str(),
at_index.value_or(-1));
return Steps(
std::move(
InstrumentNextTab(id, in_browser)
.SetDescription(base::StrCat({desc, ": Instrument Next Tab"}))),
std::move(
WithElement(
ui::test::internal::kInteractiveTestPivotElementId,
base::BindLambdaForTesting([this, url, at_index,
in_browser](ui::TrackedElement* el) {
Browser* const browser =
GetBrowserFor(el->context(), in_browser);
CHECK(browser)
<< "AddInstrumentedTab(): a browser is required.";
NavigateParams navigate_params(
browser, url, ui::PageTransition::PAGE_TRANSITION_TYPED);
navigate_params.tabstrip_index = at_index.value_or(-1);
navigate_params.disposition =
WindowOpenDisposition::NEW_FOREGROUND_TAB;
CHECK(Navigate(&navigate_params));
}))
.SetDescription(base::StrCat({desc, ": Navigate"}))),
std::move(WaitForWebContentsReady(id).FormatDescription(
base::StrCat({desc, ": %s"}))));
}
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::InstrumentNonTabWebView(ui::ElementIdentifier id,
ElementSpecifier web_view,
bool wait_for_ready) {
const auto desc = base::StringPrintf("InstrumentNonTabWebView( %s, %d, )",
id.GetName().c_str(), wait_for_ready);
auto steps = Steps(std::move(
AfterShow(web_view,
base::BindLambdaForTesting([this, id](ui::TrackedElement* el) {
test_impl().AddInstrumentedWebContents(
WebContentsInteractionTestUtil::ForNonTabWebView(
AsView<views::WebView>(el), id));
}))
.SetDescription(base::StrCat({desc, ": Instrument WebView"}))));
if (wait_for_ready) {
steps.emplace_back(std::move(WaitForWebContentsReady(id).FormatDescription(
base::StrCat({desc, ": %s"}))));
}
return steps;
}
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::InstrumentNonTabWebView(
ui::ElementIdentifier id,
AbsoluteViewSpecifier web_view,
bool wait_for_ready) {
constexpr char kTemporaryElementName[] =
"__InstrumentNonTabWebViewTemporaryElementName__";
return Steps(
std::move(NameView(kTemporaryElementName, std::move(web_view))
.FormatDescription("InstrumentNonTabWebView(): %s")),
InstrumentNonTabWebView(id, kTemporaryElementName, wait_for_ready));
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserTestApi::WaitForWebContentsReady(
ui::ElementIdentifier webcontents_id,
std::optional<GURL> expected_url) {
StepBuilder builder;
builder.SetDescription(
base::StringPrintf("WaitForWebContentsReady( %s )",
expected_url.value_or(GURL()).spec().c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
if (expected_url.has_value()) {
builder.SetStartCallback(base::BindOnce(
[](GURL expected_url, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
auto* const contents =
el->AsA<TrackedElementWebContents>()->owner()->web_contents();
if (expected_url != contents->GetURL()) {
LOG(ERROR) << "Loaded wrong URL; got " << contents->GetURL()
<< " but expected " << expected_url;
seq->FailForTesting();
}
},
expected_url.value()));
}
return builder;
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserTestApi::WaitForWebContentsNavigation(
ui::ElementIdentifier webcontents_id,
std::optional<GURL> expected_url) {
StepBuilder builder;
builder.SetDescription(
base::StringPrintf("WaitForWebContentsNavigation( %s )",
expected_url.value_or(GURL()).spec().c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
builder.SetTransitionOnlyOnEvent(true);
if (expected_url.has_value()) {
builder.SetStartCallback(base::BindOnce(
[](GURL expected_url, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
auto* const contents =
el->AsA<TrackedElementWebContents>()->owner()->web_contents();
if (expected_url != contents->GetURL()) {
LOG(ERROR) << "Loaded wrong URL; got " << contents->GetURL()
<< " but expected " << expected_url;
seq->FailForTesting();
}
},
expected_url.value()));
}
return builder;
}
// static
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::NavigateWebContents(
ui::ElementIdentifier webcontents_id,
GURL target_url) {
const auto desc = base::StringPrintf("NavigateWebContents( %s )",
target_url.spec().c_str());
return Steps(
std::move(StepBuilder()
.SetDescription(base::StrCat({desc, ": Navigate"}))
.SetElementID(webcontents_id)
.SetContext(kDefaultWebContentsContextMode)
.SetStartCallback(base::BindOnce(
[](GURL url, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
auto* const owner =
el->AsA<TrackedElementWebContents>()->owner();
if (url.EqualsIgnoringRef(
owner->web_contents()->GetURL())) {
LOG(ERROR) << "Trying to load URL " << url
<< " but WebContents URL is already "
<< owner->web_contents()->GetURL();
seq->FailForTesting();
}
owner->LoadPage(url);
},
target_url))),
std::move(WaitForWebContentsNavigation(webcontents_id, target_url)
.FormatDescription(base::StrCat({desc, ": %s"}))));
}
InteractiveBrowserTestApi::StepBuilder
InteractiveBrowserTestApi::FocusWebContents(
ui::ElementIdentifier webcontents_id) {
StepBuilder builder;
builder.SetElementID(webcontents_id);
builder.SetDescription("FocusWebContents()");
builder.SetStartCallback(base::BindLambdaForTesting(
[this](ui::InteractionSequence* seq, ui::TrackedElement* el) {
auto* const tracked_el = AsInstrumentedWebContents(el);
if (!tracked_el) {
LOG(ERROR) << "Element is not an instrumented WebContents.";
seq->FailForTesting();
return;
}
const auto result = test_util().ActivateSurface(el);
test_impl().HandleActionResult(seq, el, "ActivateSurface", result);
if (result != ui::test::ActionResult::kSucceeded) {
return;
}
auto* const contents = tracked_el->web_contents();
if (!contents || !contents->GetPrimaryMainFrame()) {
LOG(ERROR) << "WebContents not present or no main frame.";
seq->FailForTesting();
return;
}
content::UpdateUserActivationStateInterceptor
user_activation_interceptor(contents->GetPrimaryMainFrame());
user_activation_interceptor.UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
}));
return builder;
}
// static
InteractiveBrowserTestApi::MultiStep
InteractiveBrowserTestApi::WaitForStateChange(
ui::ElementIdentifier webcontents_id,
const StateChange& state_change,
bool expect_timeout) {
ui::CustomElementEventType event_type =
expect_timeout ? state_change.timeout_event : state_change.event;
CHECK(event_type);
std::ostringstream desc;
desc << "WaitForStateChange( " << state_change << ", "
<< (expect_timeout ? "true" : "false") << " )";
const bool fail_on_close = !state_change.continue_across_navigation;
return Steps(
std::move(StepBuilder()
.SetDescription(base::StrCat({desc.str(), ": Queue Event"}))
.SetElementID(webcontents_id)
.SetContext(kDefaultWebContentsContextMode)
.SetMustRemainVisible(fail_on_close)
.SetStartCallback(base::BindOnce(
[](StateChange state_change, ui::TrackedElement* el) {
el->AsA<TrackedElementWebContents>()
->owner()
->SendEventOnStateChange(state_change);
},
state_change))),
std::move(
StepBuilder()
.SetDescription(base::StrCat({desc.str(), ": Wait For Event"}))
.SetElementID(webcontents_id)
.SetContext(
ui::InteractionSequence::ContextMode::kFromPreviousStep)
.SetType(ui::InteractionSequence::StepType::kCustomEvent,
event_type)
.SetMustBeVisibleAtStart(fail_on_close)));
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::EnsurePresent(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where) {
StepBuilder builder;
builder.SetDescription(base::StringPrintf(
"EnsurePresent( %s, %s )", webcontents_id.GetName().c_str(),
internal::InteractiveBrowserTestPrivate::DeepQueryToString(where)
.c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
builder.SetStartCallback(base::BindOnce(
[](DeepQuery where, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
if (!AsInstrumentedWebContents(el)->Exists(where)) {
LOG(ERROR) << "Expected DOM element to be present: " << where;
seq->FailForTesting();
}
},
where));
return builder;
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserTestApi::EnsureNotPresent(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where) {
StepBuilder builder;
builder.SetDescription(base::StringPrintf(
"EnsureNotPresent( %s, %s )", webcontents_id.GetName().c_str(),
internal::InteractiveBrowserTestPrivate::DeepQueryToString(where)
.c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
builder.SetStartCallback(base::BindOnce(
[](DeepQuery where, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
if (AsInstrumentedWebContents(el)->Exists(where)) {
LOG(ERROR) << "Expected DOM element not to be present: " << where;
seq->FailForTesting();
}
},
where));
return builder;
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::ExecuteJs(
ui::ElementIdentifier webcontents_id,
const std::string& function,
ExecuteJsMode mode) {
StepBuilder builder;
builder.SetDescription(
base::StringPrintf("ExecuteJs(\"\n%s\n\")", function.c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
switch (mode) {
case ExecuteJsMode::kFireAndForget:
builder.SetMustRemainVisible(false);
builder.SetStartCallback(base::BindOnce(
[](std::string function, ui::TrackedElement* el) {
AsInstrumentedWebContents(el)->Execute(function);
},
function));
break;
case ExecuteJsMode::kWaitForCompletion:
builder.SetStartCallback(base::BindOnce(
[](std::string function, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
const auto full_function = base::StringPrintf(
"() => { (%s)(); return false; }", function.c_str());
std::string error_msg;
AsInstrumentedWebContents(el)->Evaluate(full_function, &error_msg);
if (!error_msg.empty()) {
LOG(ERROR) << "ExecuteJsAt() failed: " << error_msg;
seq->FailForTesting();
}
},
function));
break;
}
return builder;
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::ExecuteJsAt(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where,
const std::string& function,
ExecuteJsMode mode) {
StepBuilder builder;
builder.SetDescription(base::StringPrintf(
"ExecuteJsAt( %s, \"\n%s\n\")",
internal::InteractiveBrowserTestPrivate::DeepQueryToString(where).c_str(),
function.c_str()));
builder.SetElementID(webcontents_id);
builder.SetContext(kDefaultWebContentsContextMode);
switch (mode) {
case ExecuteJsMode::kFireAndForget:
builder.SetMustRemainVisible(false);
builder.SetStartCallback(base::BindOnce(
[](DeepQuery where, std::string function, ui::TrackedElement* el) {
AsInstrumentedWebContents(el)->ExecuteAt(where, function);
},
where, function));
break;
case ExecuteJsMode::kWaitForCompletion:
builder.SetStartCallback(base::BindOnce(
[](DeepQuery where, std::string function,
ui::InteractionSequence* seq, ui::TrackedElement* el) {
const auto full_function = base::StringPrintf(
R"(
(el, err) => {
if (err) {
throw err;
}
(%s)(el);
return false;
}
)",
function.c_str());
std::string error_msg;
AsInstrumentedWebContents(el)->EvaluateAt(where, full_function,
&error_msg);
if (!error_msg.empty()) {
LOG(ERROR) << "ExecuteJsAt() failed: " << error_msg;
seq->FailForTesting();
}
},
where, function));
break;
}
return builder;
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::CheckJsResult(
ui::ElementIdentifier webcontents_id,
const std::string& function) {
return CheckJsResult(webcontents_id, function,
testing::Matcher<base::Value>(IsTruthyMatcher()));
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserTestApi::CheckJsResultAt(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where,
const std::string& function) {
return CheckJsResultAt(webcontents_id, where, function,
testing::Matcher<base::Value>(IsTruthyMatcher()));
}
InteractiveBrowserTestApi::StepBuilder InteractiveBrowserTestApi::MoveMouseTo(
ElementSpecifier web_contents,
const DeepQuery& where) {
return MoveMouseTo(web_contents, DeepQueryToRelativePosition(where));
}
InteractiveBrowserTestApi::StepBuilder InteractiveBrowserTestApi::DragMouseTo(
ElementSpecifier web_contents,
const DeepQuery& where,
bool release) {
return DragMouseTo(web_contents, DeepQueryToRelativePosition(where), release);
}
InteractiveBrowserTestApi::StepBuilder
InteractiveBrowserTestApi::ScrollIntoView(ui::ElementIdentifier web_contents,
const DeepQuery& where) {
return std::move(
ExecuteJsAt(web_contents, where,
"(el) => { el.scrollIntoView({ behavior: 'instant' }); }")
.SetDescription("ScrollIntoView()"));
}
// static
InteractiveBrowserTestApi::RelativePositionCallback
InteractiveBrowserTestApi::DeepQueryToRelativePosition(const DeepQuery& query) {
return base::BindOnce(
[](DeepQuery q, ui::TrackedElement* el) {
auto* const contents = el->AsA<TrackedElementWebContents>();
const gfx::Rect container_bounds = contents->GetScreenBounds();
const gfx::Rect element_bounds =
contents->owner()->GetElementBoundsInScreen(q);
CHECK(!element_bounds.IsEmpty())
<< "Cannot target DOM element at " << q << " in "
<< el->identifier() << " because its screen bounds are emtpy.";
gfx::Rect intersect_bounds = element_bounds;
intersect_bounds.Intersect(container_bounds);
CHECK(!intersect_bounds.IsEmpty())
<< "Cannot target DOM element at " << q << " in "
<< el->identifier() << " because its screen bounds "
<< element_bounds.ToString()
<< " are outside the screen bounds of the containing WebView, "
<< container_bounds.ToString()
<< ". Did you forget to scroll the element into view? See "
"ScrollIntoView().";
return intersect_bounds.CenterPoint();
},
query);
}
Browser* InteractiveBrowserTestApi::GetBrowserFor(
ui::ElementContext current_context,
BrowserSpecifier spec) {
return absl::visit(
base::Overloaded{[](AnyBrowser) -> Browser* { return nullptr; },
[current_context](CurrentBrowser) {
Browser* const browser =
InteractionTestUtilBrowser::GetBrowserFromContext(
current_context);
CHECK(browser) << "Current context is not a browser.";
return browser;
},
[](Browser* browser) {
CHECK(browser)
<< "BrowserSpecifier: Browser* is null.";
return browser;
},
[](std::reference_wrapper<Browser*> browser) {
CHECK(browser.get())
<< "BrowserSpecifier: Browser* is null.";
return browser.get();
}},
spec);
}