blob: 6505c279116125e98d202718b919f5adc06de5fb [file] [log] [blame]
// Copyright 2025 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_window_test.h"
#include <sstream>
#include <string>
#include <utility>
#include <variant>
#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/test_switches.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/interaction/browser_elements.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 "components/tabs/public/tab_interface.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.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/functional/overload.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/base/test/ui_controls.h"
namespace {
// Checks that an element is visible in a non-empty region of the viewport.
// Perhaps look into using checkVisibility() in the future, but this approach
// seems most robust.
constexpr char kElementVisibilityQuery[] =
R"(
function(el) {
const rect = el.getBoundingClientRect();
const left = Math.max(0, rect.x);
const top = Math.max(0, rect.y);
const right = Math.min(rect.x + rect.width, window.innerWidth);
const bottom = Math.min(rect.y + rect.height, window.innerHeight);
return right > left && bottom > top;
}
)";
// Returns the intersection rectangle between a Web UI element `where`, and the
// tracked WebContents `el` it resides in. The web element must reside (at least
// partially) within the web container's bounds and be visible on screen, or
// this will CHECK() fail.
gfx::Rect GetWebElementIntersection(
ui::TrackedElement* el,
const WebContentsInteractionTestUtil::DeepQuery& where) {
auto* const contents = el->AsA<TrackedElementWebContents>();
CHECK(contents) << "Containing element is not a WebContents";
const gfx::Rect container_bounds = contents->GetScreenBounds();
gfx::Rect element_bounds = contents->owner()->GetElementBoundsInScreen(where);
CHECK(!element_bounds.IsEmpty())
<< "Cannot target DOM element at " << where << " 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 " << where << " in " << el->identifier()
<< " because its screen bounds " << element_bounds.ToString()
<< " are outside the screen bounds of the containing frame, "
<< container_bounds.ToString()
<< ". Did you forget to scroll the element into view? See "
"ScrollIntoView().";
return intersect_bounds;
}
// Returns the location of Web UI element `where`, relative to the tracked
// WebContents `el` it resides in. The web element must reside (at least
// partially) within the web container's bounds and be visible on screen, or
// this will CHECK() fail.
gfx::Rect GetRegionInWebContents(
ui::TrackedElement* el,
const WebContentsInteractionTestUtil::DeepQuery& where) {
gfx::Rect intersect_bounds = GetWebElementIntersection(el, where);
// Compute the sub-region relative to the webcontents.
auto* const contents = el->AsA<TrackedElementWebContents>();
CHECK(contents) << "Containing element is not a WebContents";
const gfx::Rect container_bounds = contents->GetScreenBounds();
intersect_bounds.Offset(-container_bounds.OffsetFromOrigin());
return intersect_bounds;
}
// Returns the browser window that `web_contents` is a tab in, or nullptr if it
// is not in a known tab.
BrowserWindowInterface* MaybeGetForWebContentsInTab(
content::WebContents* web_contents) {
if (auto* tab = tabs::TabInterface::MaybeGetFromContents(web_contents)) {
return tab->GetBrowserWindowInterface();
}
return nullptr;
}
} // namespace
DEFINE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(InteractiveBrowserWindowTestApi,
kDefaultWaitForJsResultEvent);
DEFINE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(InteractiveBrowserWindowTestApi,
kDefaultWaitForJsResultAtEvent);
InteractiveBrowserWindowTestApi::InteractiveBrowserWindowTestApi()
: test_impl_(private_test_impl()
.MaybeRegisterFrameworkImpl<
internal::InteractiveBrowserTestPrivate>()) {}
InteractiveBrowserWindowTestApi::~InteractiveBrowserWindowTestApi() = default;
// static
WebContentsInteractionTestUtil*
InteractiveBrowserWindowTestApi::AsInstrumentedWebContents(
ui::TrackedElement* el) {
auto* const web_el = el->AsA<TrackedElementWebContents>();
CHECK(web_el);
return web_el->owner();
}
void InteractiveBrowserWindowTestApi::EnableWebUICodeCoverage() {
test_impl_->MaybeStartWebUICodeCoverage();
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::ScreenshotWebUi(
ElementSpecifier element,
const DeepQuery& where,
const std::string& screenshot_name,
const std::string& baseline_cl) {
StepBuilder builder;
builder.SetDescription("Compare WebUI Element Screenshot");
builder.SetElement(element);
builder.SetStartCallback(base::BindOnce(
[](InteractiveBrowserWindowTestApi* test, std::string screenshot_name,
std::string baseline_cl, const DeepQuery& where,
ui::InteractionSequence* seq, ui::TrackedElement* el) {
// Locate the element within the bounds of the WebContents.
const auto window_rect = GetRegionInWebContents(el, where);
ScreenshotOptions options;
options.region = window_rect;
options.focus = ScreenshotFocusMode::kLeaveFocusWhereItIs;
const auto result = InteractionTestUtilBrowser::CompareScreenshot(
el, screenshot_name, baseline_cl, options);
test->private_test_impl().HandleActionResult(seq, el, "Screenshot",
result);
},
base::Unretained(this), screenshot_name, baseline_cl, where));
auto steps = Steps(MaybeWaitForPaint(element), std::move(builder),
MaybeWaitForUserToDismiss(element));
AddDescriptionPrefix(
steps, base::StrCat({"ScreenshotWebUi( ", "", screenshot_name, ", ", "",
baseline_cl, ""}));
return steps;
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::ScreenshotSurface(
ElementSpecifier element_in_surface,
const std::string& screenshot_name,
const std::string& baseline_cl) {
StepBuilder builder;
builder.SetDescription("Compare Surface Screenshot");
builder.SetElement(element_in_surface);
builder.SetStartCallback(base::BindOnce(
[](InteractiveBrowserWindowTestApi* test, std::string screenshot_name,
std::string baseline_cl, ui::InteractionSequence* seq,
ui::TrackedElement* el) {
const auto result =
InteractionTestUtilBrowser::CompareSurfaceScreenshot(
el, screenshot_name, baseline_cl);
test->private_test_impl().HandleActionResult(seq, el, "Screenshot",
result);
},
base::Unretained(this), screenshot_name, baseline_cl));
auto steps = Steps(MaybeWaitForPaint(element_in_surface), std::move(builder),
MaybeWaitForUserToDismiss(element_in_surface));
AddDescriptionPrefix(
steps, base::StrCat({"ScreenshotSurface( \"", screenshot_name, "\", \"",
baseline_cl, "\" )"}));
return steps;
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::InstrumentTab(ui::ElementIdentifier id,
std::optional<int> tab_index,
BrowserSpecifier in_browser,
bool wait_for_ready) {
auto steps = Steps(WithElement(
ui::test::internal::kInteractiveTestPivotElementId,
base::BindLambdaForTesting([this, id, tab_index,
in_browser](ui::TrackedElement* el) {
auto* const browser = GetBrowserWindowFor(el->context(), in_browser);
CHECK(browser) << "InstrumentTab(): a specific browser is required.";
test_impl_->AddInstrumentedWebContents(
WebContentsInteractionTestUtil::ForExistingTabInBrowser(browser, id,
tab_index));
})));
if (wait_for_ready) {
steps.push_back(WaitForWebContentsReady(id));
}
AddDescriptionPrefix(
steps,
base::StringPrintf("InstrumentTab( %s, %d, %d )", id.GetName().c_str(),
tab_index.value_or(-1), wait_for_ready));
return steps;
}
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::InstrumentNextTab(
ui::ElementIdentifier id,
BrowserSpecifier in_browser) {
return WithElement(
ui::test::internal::kInteractiveTestPivotElementId,
[this, id, in_browser](ui::TrackedElement* el) {
auto* const browser =
GetBrowserWindowFor(el->context(), in_browser);
test_impl_->AddInstrumentedWebContents(
browser
? WebContentsInteractionTestUtil::ForNextTabInBrowser(
browser, id)
: WebContentsInteractionTestUtil::ForNextTabInAnyBrowser(
id));
})
.AddDescriptionPrefix(
base::StrCat({"InstrumentTab( ", id.GetName(), " )"}));
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::AddInstrumentedTab(
ui::ElementIdentifier id,
GURL url,
std::optional<int> at_index,
BrowserSpecifier in_browser) {
auto steps = Steps(
InstrumentNextTab(id, in_browser),
WithElement(
ui::test::internal::kInteractiveTestPivotElementId,
base::BindLambdaForTesting([this, url, at_index,
in_browser](ui::TrackedElement* el) {
auto* const browser =
GetBrowserWindowFor(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));
})),
WaitForWebContentsReady(id));
AddDescriptionPrefix(
steps, base::StringPrintf("AddInstrumentedTab( %s, %s, %d, )",
id.GetName().c_str(), url.spec().c_str(),
at_index.value_or(-1)));
return steps;
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::InstrumentInnerWebContents(
ui::ElementIdentifier inner_id,
ui::ElementIdentifier outer_id,
size_t inner_contents_index,
bool wait_for_ready) {
MultiStep steps;
steps.emplace_back(Do([this, inner_id, outer_id, inner_contents_index]() {
test_impl_->AddInstrumentedWebContents(
WebContentsInteractionTestUtil::ForInnerWebContents(
outer_id, inner_contents_index, inner_id));
}));
if (wait_for_ready) {
steps.push_back(WaitForWebContentsReady(inner_id));
}
AddDescriptionPrefix(
steps, base::StringPrintf("InstrumentInnerWebContents( %s, %s, %u, %d )",
inner_id.GetName(), outer_id.GetName(),
inner_contents_index, wait_for_ready));
return steps;
}
InteractiveBrowserWindowTestApi::StepBuilder
InteractiveBrowserWindowTestApi::UninstrumentWebContents(
ui::ElementIdentifier id,
bool fail_if_not_instrumented) {
return std::move(
(fail_if_not_instrumented
? Check([this, id]() {
return test_impl_->UninstrumentWebContents(id);
})
: Do([this, id]() { test_impl_->UninstrumentWebContents(id); }))
.SetDescription(
base::StringPrintf("UninstrumentWebContents(%s)", id.GetName())));
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::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);
// Because we're checking the current specific state of the contents, this
// avoids further navigations breaking the test.
builder.SetStepStartMode(ui::InteractionSequence::StepStartMode::kImmediate);
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
InteractiveBrowserWindowTestApi::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;
}
// There is a bug that causes WebContents::CompletedFirstVisuallyNonEmptyPaint()
// to occasionally fail to ever become true. This sometimes manifests when
// running tests on Mac builders. In order to prevent tests from hanging when
// trying to ensure a non-empty paint, then, a workaround is required.
//
// See https://crbug.com/332895669 and https://crbug.com/334747109 for more
// information.
namespace {
// Warning message so people aren't surprised when something else in their test
// flakes after this step due to the bug.
constexpr char kPaintWorkaroundWarning[] =
"\n\nIMPORTANT NOTE FOR TESTERS AND CHROMIUM GARDENERS:\n\n"
"There is a known issue (crbug.com/332895669, crbug.com/334747109) on Mac "
"and Win where sometimes WebContents::CompletedFirstVisuallyNonEmptyPaint()"
" can return false even for a WebContents that is visible and painted, "
"especially in secondary UI.\n\n"
"Unfortunately, this has happened. In order to prevent this test from "
"timing out, we will be ensuring that the page is visible and renders at "
"least one frame and then continuing the test.\n\n"
"In most cases, this will only result in a slight delay. However, in a "
"handful of cases the test may hang or fail because some other code relies "
"on the page reporting as painted, which we have no direct control over. "
"If this happens, you may need to disable the test until the lower-level "
"bug is fixed.\n";
// CheckJsResult() can handle promises, so queue a promise that only succeeds
// after the contents have been rendered.
constexpr char kPaintWorkaroundFunction[] =
"() => new Promise(resolve => requestAnimationFrame(() => resolve(true)))";
// Event sent on a delay to bypass the "was this WebContents painted?" check on
// platforms where the check is flaky; see comments above.
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kPaintWorkaroundEvent);
void MaybePostPaintWorkaroundEvent(ui::TrackedElement* el) {
// Only secondary web contents are affected.
if (MaybeGetForWebContentsInTab(
el->AsA<TrackedElementWebContents>()->owner()->web_contents())) {
return;
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](ui::ElementIdentifier id) {
if (auto* const el = ui::ElementTracker::GetElementTracker()
->GetElementInAnyContext(id)) {
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
el, kPaintWorkaroundEvent);
}
},
el->identifier()),
base::Seconds(1));
}
} // namespace
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::WaitForWebContentsPainted(
ui::ElementIdentifier webcontents_id) {
auto wait_step = WaitForEvent(webcontents_id,
TrackedElementWebContents::kFirstNonEmptyPaint);
wait_step.SetContext(kDefaultWebContentsContextMode);
wait_step.SetMustBeVisibleAtStart(false);
wait_step.AddDescriptionPrefix("WaitForWebContentsPainted()");
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
const bool requires_workaround = true;
#else
const bool requires_workaround = false;
#endif
if (requires_workaround) {
// Workaround for https://crbug.com/332895669 and
// https://crbug.com/334747109:
//
// In parallel with waiting for the WebContents to report as painted, post a
// delayed event, verify the contents are visible, and ensure at least one
// frame has rendered. This doesn't fix the problem of the WebContents not
// reporting as painted, but it does prevent tests that want to ensure that
// the contents *are* painted from hanging.
wait_step = AnyOf(
// Ideally this finishes pretty quickly and we can move on.
RunSubsequence(std::move(wait_step)),
// Otherwise, create a timeout after the WebContents is shown.
RunSubsequence(
// Ensure that the contents are loaded, then wait a short time.
InAnyContext(
AfterShow(webcontents_id, &MaybePostPaintWorkaroundEvent)),
// After the timeout, first post a verbose warning describing the
// known issue so that test maintainers are not surprised if
// something later in the test breaks because paint status is still
// being reported incorrectly.
InSameContext(
AfterEvent(webcontents_id, kPaintWorkaroundEvent,
[]() { LOG(WARNING) << kPaintWorkaroundWarning; }),
// Ensure that the WebContents actually believes it's visible.
CheckElement(
webcontents_id,
[](ui::TrackedElement* el) {
return AsInstrumentedWebContents(el)
->web_contents()
->GetVisibility();
},
content::Visibility::VISIBLE),
// Force a frame to render before proceeding.
// After this is done, we at least known that the contents have
// been painted - even if the WebContents object itself doesn't!
CheckJsResult(webcontents_id, kPaintWorkaroundFunction))));
}
// If the element is already painted, there is no reason to actually wait (and
// in fact that will cause a timeout). So only execute the wait step if the
// WebContents is not ready or not painted.
//
// Note: this could also be done with a custom `StateObserver` and
// `WaitForState()` but this approach requires the fewest steps.
return IfElement(
webcontents_id,
[](const ui::TrackedElement* el) {
// If the page is not ready (i.e. no element) or not
// painted, execute the wait step; otherwise skip it.
return !el || !el->AsA<TrackedElementWebContents>()
->owner()
->HasPageBeenPainted();
},
Then(std::move(wait_step)))
.SetContext(kDefaultWebContentsContextMode)
.AddDescriptionPrefix("WaitForWebContentsPainted()");
}
// static
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::NavigateWebContents(
ui::ElementIdentifier webcontents_id,
GURL target_url) {
auto steps = Steps(
std::move(StepBuilder()
.SetDescription("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))),
WaitForWebContentsNavigation(webcontents_id, target_url));
AddDescriptionPrefix(
steps, base::StrCat({"NavigateWebContents( ", target_url.spec(), " )"}));
return steps;
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::FocusWebContents(
ui::ElementIdentifier webcontents_id) {
auto steps = InAnyContext(WaitForWebContentsPainted(webcontents_id),
ActivateSurface(webcontents_id),
FocusElement(webcontents_id));
AddDescriptionPrefix(steps, "FocusWebContents()");
return steps;
}
// static
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::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);
const bool fail_on_close = !state_change.continue_across_navigation;
StepBuilder step1;
step1.SetDescription("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));
if (state_change.continue_across_navigation) {
// This is required to prevent failing if the element would otherwise be
// hidden due to a navigation between trigger and step start.
step1.SetStepStartMode(ui::InteractionSequence::StepStartMode::kImmediate);
}
auto steps = Steps(
std::move(step1),
std::move(StepBuilder()
.SetDescription("Wait For Event")
.SetElementID(webcontents_id)
.SetContext(
ui::InteractionSequence::ContextMode::kFromPreviousStep)
.SetType(ui::InteractionSequence::StepType::kCustomEvent,
event_type)
.SetMustBeVisibleAtStart(fail_on_close)));
AddDescriptionPrefix(
steps, base::StrCat({"WaitForStateChange( ", base::ToString(state_change),
", ", base::ToString(expect_timeout), " )"}));
return steps;
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::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
InteractiveBrowserWindowTestApi::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;
}
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::EnsureNotVisible(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where) {
return IfElement(
webcontents_id,
[where](const ui::TrackedElement* el) {
return el->AsA<TrackedElementWebContents>()->owner()->Exists(where);
},
Then(CheckJsResultAt(webcontents_id, where, kElementVisibilityQuery,
false)));
}
// static
ui::InteractionSequence::StepBuilder InteractiveBrowserWindowTestApi::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
InteractiveBrowserWindowTestApi::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
InteractiveBrowserWindowTestApi::CheckJsResult(
ui::ElementIdentifier webcontents_id,
const std::string& function) {
return CheckJsResult(webcontents_id, function, internal::IsTruthyMatcher());
}
// static
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::CheckJsResultAt(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where,
const std::string& function) {
return CheckJsResultAt(webcontents_id, where, function,
internal::IsTruthyMatcher());
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForJsResult(
ui::ElementIdentifier webcontents_id,
const std::string& function) {
return WaitForJsResult(webcontents_id, function, IsTruthy());
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForJsResultAt(
ui::ElementIdentifier webcontents_id,
const DeepQuery& where,
const std::string& function) {
return WaitForJsResultAt(webcontents_id, where, function, IsTruthy());
}
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::ScrollIntoView(
ui::ElementIdentifier web_contents,
const DeepQuery& where) {
return std::move(
ExecuteJsAt(web_contents, where,
"(el) => { el.scrollIntoView({ behavior: 'instant' }); }")
.SetDescription("ScrollIntoView()"));
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::WaitForElementVisible(
ui::ElementIdentifier web_contents,
const DeepQuery& where) {
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kWaitforElementVisibleCompleteEvent);
StateChange change;
change.event = kWaitforElementVisibleCompleteEvent;
change.test_function = kElementVisibilityQuery;
change.type = StateChange::Type::kExistsAndConditionTrue;
change.where = where;
auto steps = WaitForStateChange(web_contents, change);
AddDescriptionPrefix(steps, "WaitForElementVisible()");
return steps;
}
ui::InteractionSequence::StepBuilder
InteractiveBrowserWindowTestApi::ClickElement(
ui::ElementIdentifier web_contents,
const DeepQuery& where,
ui_controls::MouseButton button,
ui_controls::AcceleratorState modifiers,
ExecuteJsMode execute_mode) {
int js_button;
switch (button) {
case ui_controls::LEFT:
js_button = 0;
break;
case ui_controls::MIDDLE:
js_button = 1;
break;
case ui_controls::RIGHT:
js_button = 2;
break;
}
const bool shift = modifiers & ui_controls::kShift;
const bool alt = modifiers & ui_controls::kAlt;
const bool ctrl = modifiers & ui_controls::kControl;
const bool meta = modifiers & ui_controls::kCommand;
auto b2s = [](bool b) { return base::ToString(b); };
const std::string command = base::StringPrintf(
R"(
function(el) {
const rect = el.getBoundingClientRect();
const left = Math.max(0, rect.x);
const top = Math.max(0, rect.y);
const right = Math.min(rect.x + rect.width, window.innerWidth);
const bottom = Math.min(rect.y + rect.height, window.innerHeight);
if (right <= left || bottom <= top) {
throw new Error(
'Target element is zero size or ' +
'has empty intersection with the viewport.');
}
const x = (left + right) / 2;
const y = (top + bottom) / 2;
const event = new MouseEvent(
'click',
{
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: %d,
shiftKey: %s,
altKey: %s,
ctrlKey: %s,
metaKey: %s
}
);
el.dispatchEvent(event);
}
)",
js_button, b2s(shift), b2s(alt), b2s(ctrl), b2s(meta));
return ExecuteJsAt(web_contents, where, command, execute_mode)
.SetDescription("ClickElement()");
}
// static
InteractiveBrowserWindowTestApi::RelativePositionCallback
InteractiveBrowserWindowTestApi::DeepQueryToRelativePosition(
const DeepQuery& where) {
return base::BindOnce(
[](DeepQuery q, ui::TrackedElement* el) {
gfx::Rect intersect_bounds = GetWebElementIntersection(el, q);
return intersect_bounds.CenterPoint();
},
where);
}
InteractiveBrowserWindowTestApi::MultiStep
InteractiveBrowserWindowTestApi::MaybeWaitForPaint(ElementSpecifier element) {
// Only wait if `element` is actually a `WebContents`.
//
// WebContents are typically only referred to via their assigned IDs.
// TODO(dfried): possibly handle (rare) cases where a name has been assigned.
if (!element.is_identifier()) {
return MultiStep();
}
const auto element_id = element.identifier();
// Do a `WaitForWebContentsPainted()`, but only if the ID has been assigned to
// an instrumented `WebContents`.
return Steps(If(
[this, element_id]() {
return test_impl_->IsInstrumentedWebContents(element_id);
},
Then(WaitForWebContentsPainted(element_id))));
}
// static
InteractiveBrowserWindowTestApi::StepBuilder
InteractiveBrowserWindowTestApi::MaybeWaitForUserToDismiss(
ElementSpecifier element) {
// In interactive mode (--test-launcher-interactive) the behavior for pixel
// tests is to wait until the user closes/hides the surface that has the
// element to screenshot. This may break the rest of the test, but it's fine
// because the purpose of interactive mode is for a human user to observe
// what the test sees during the screenshot step.
return If(
[]() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kTestLauncherInteractive);
},
Then(Log(
R"(
------------------
Since --test-launcher-interactive is specified, this test will now wait for you
to dismiss the element that is being screenshot:
)",
" ", element,
R"(
Note that This may cause the remainder of the test to fail or crash, if the test
does not expect the surface to be dismissed.
------------------
)"),
WaitForHide(element)));
}
BrowserWindowInterface* InteractiveBrowserWindowTestApi::GetBrowserWindowFor(
ui::ElementContext current_context,
BrowserSpecifier spec) {
return std::visit(
absl::Overload{
[](AnyBrowser) -> BrowserWindowInterface* { return nullptr; },
[current_context](CurrentBrowser) {
BrowserWindowInterface* const browser =
InteractionTestUtilBrowser::GetBrowserFromContext(
current_context);
CHECK(browser) << "Current context is not a browser window.";
return browser;
},
[](BrowserWindowInterface* browser) {
CHECK(browser) << "BrowserSpecifier: browser window is null.";
return browser;
},
[](std::reference_wrapper<BrowserWindowInterface*> browser) {
CHECK(browser.get()) << "BrowserSpecifier: browser window is null.";
return browser.get();
}},
spec);
}