blob: 20ebd1f8079532986e27de4da98dfc2eaee42eee [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
#include "cc/base/features.h"
#include "components/input/render_widget_host_input_event_router.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/renderer_host/unbounded_surface_window.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/input/synthetic_gesture_target.h"
#include "content/common/input/synthetic_pointer_driver.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "url/gurl.h"
#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/aura/window_tracker.h"
#endif
#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif
#if BUILDFLAG(IS_ANDROID)
#include "base/android/android_info.h"
#endif
namespace content {
namespace {
void SkipTestsForUnsupportedPlatforms() {
#if BUILDFLAG(IS_IOS)
// TODO(crbug.com/508672616): Not yet implemented on iOS.
GTEST_SKIP();
#elif BUILDFLAG(IS_ANDROID)
// if (base::android::android_info::sdk_int() <
// base::android::android_info::SDK_VERSION_U) {
// GTEST_SKIP()
// << "Unbounded elements require Android U (API 34+ / Android 14+).";
// }
// TODO(crbug.com/544212552): Flaky/failing on Android.
GTEST_SKIP();
#elif BUILDFLAG(IS_OZONE)
if (ui::OzonePlatform::RunningOnWaylandForTest()) {
// TODO(crbug.com/523970924): Wayland is flaky.
GTEST_SKIP();
}
#endif
}
} // namespace
class UnboundedElementBrowserTestBase : public ContentBrowserTest {
public:
UnboundedElementBrowserTestBase() = default;
~UnboundedElementBrowserTestBase() override = default;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
IsolateAllSitesForTesting(command_line);
command_line->AppendSwitchASCII(
switches::kTouchEventFeatureDetection,
switches::kTouchEventFeatureDetectionEnabled);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
void TearDownOnMainThread() override {
if (base::FeatureList::IsEnabled(blink::features::kUnboundedElement)) {
UnboundedSurfaceWindow* window =
primary_main_frame_host()->GetUnboundedSurfaceWindow();
if (window) {
auto tracker = CreateDestructionTracker(*window);
window->Dismiss();
WaitForDestruction(std::move(tracker));
}
}
ContentBrowserTest::TearDownOnMainThread();
}
UnboundedSurfaceWindow* GetActiveWindow() {
if (!base::test::RunUntil([&]() {
return primary_main_frame_host()->GetUnboundedSurfaceWindow() !=
nullptr;
})) {
return nullptr;
}
return primary_main_frame_host()->GetUnboundedSurfaceWindow();
}
#if defined(USE_AURA)
// Native widget destruction is async with Aura, hence we define helpers to
// wait for destruction.
std::unique_ptr<aura::WindowTracker> CreateDestructionTracker(
UnboundedSurfaceWindow& window) {
auto tracker = std::make_unique<aura::WindowTracker>();
gfx::NativeWindow native_window = window.GetNativeWindow();
if (!native_window) {
return tracker;
}
tracker->Add(native_window);
#if BUILDFLAG(IS_WIN)
// Explicitly wait for the top-level window to be destroyed on windows, to
// avoid the test runner's leak detection check from failing.
if (native_window->parent()) {
tracker->Add(native_window->parent());
}
#endif
return tracker;
}
void WaitForDestruction(std::unique_ptr<aura::WindowTracker> tracker) {
EXPECT_TRUE(
base::test::RunUntil([&]() { return tracker->windows().empty(); }));
}
#else
// Stub implementations for non-Aura.
int CreateDestructionTracker(UnboundedSurfaceWindow& window) { return 0; }
void WaitForDestruction(int tracker) { return; }
#endif // defined(USE_AURA)
WebContentsImpl* web_contents() const {
return static_cast<WebContentsImpl*>(shell()->web_contents());
}
RenderFrameHostImpl* primary_main_frame_host() {
return web_contents()->GetPrimaryFrameTree().root()->current_frame_host();
}
void WaitForFrameReady() {
ASSERT_NE(GetActiveWindow(), nullptr);
WaitForHitTestData(primary_main_frame_host());
MainThreadFrameObserver frame_observer(
primary_main_frame_host()->GetRenderWidgetHost());
frame_observer.Wait();
}
};
class UnboundedElementBrowserTest : public UnboundedElementBrowserTestBase,
public testing::WithParamInterface<bool> {
public:
UnboundedElementBrowserTest() = default;
~UnboundedElementBrowserTest() override = default;
void SetUp() override {
SkipTestsForUnsupportedPlatforms();
std::vector<base::test::FeatureRef> enabled_features = {
blink::features::kUnboundedElement,
blink::features::kUnsafeUnboundedElementOnTheOpenWeb};
std::vector<base::test::FeatureRef> disabled_features;
if (GetParam()) {
enabled_features.push_back(::features::kTreesInViz);
} else {
disabled_features.push_back(::features::kTreesInViz);
}
feature_list_.InitWithFeatures(enabled_features, disabled_features);
UnboundedElementBrowserTestBase::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
class UnboundedElementEscapingOpenWebBrowserTest
: public base::test::WithFeatureOverride,
public UnboundedElementBrowserTestBase {
public:
UnboundedElementEscapingOpenWebBrowserTest()
: base::test::WithFeatureOverride(::features::kTreesInViz) {}
~UnboundedElementEscapingOpenWebBrowserTest() override = default;
void SetUp() override {
SkipTestsForUnsupportedPlatforms();
UnboundedElementBrowserTestBase::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_{{
blink::features::kUnboundedElement,
blink::features::kUnsafeEscapingUnboundedElementOnTheOpenWeb,
}};
};
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, ActivationPreconditions) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Create an unbounded element via HTML snippet:
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement().catch(e => e.name);
)";
// showUnboundedElement throws DOMException NotAllowedError without transient
// user gesture.
EXPECT_EQ("NotAllowedError", EvalJs(primary_main_frame_host(), script,
EXECUTE_SCRIPT_NO_USER_GESTURE));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
WebUIPrivilegedBypassesUserActivation) {
GURL webui_url = GURL(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
// Use DOM APIs instead of innerHTML because WebUI pages enforce TrustedTypes
// with a strict CSP ("trusted-types static-types;") that disallows creating
// custom policies in test scripts.
std::string script = R"(
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1';
document.head.appendChild(meta);
const div = document.createElement('div');
div.id = 'target';
div.setAttribute('unbounded', '');
document.body.appendChild(div);
div.showUnboundedElement().then(() => "Success", e => e.name);
)";
// Since it's a privileged WebUI page, it should bypass the transient user
// activation requirement.
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), script,
EXECUTE_SCRIPT_NO_USER_GESTURE));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, AncestorClipping) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string setup_script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="container" style="width:50px; height:50px; overflow:hidden;
position:relative;">
<div id="child" style="width:100px; height:100px; position:absolute;
top:0; left:0;" unbounded></div>
</div>
`;
const child = document.getElementById('child');
child.addEventListener('mousedown', () => { window.__clicked = true; });
child.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
WaitForFrameReady();
SimulateMouseClickAt(web_contents(), 0, blink::WebMouseEvent::Button::kLeft,
gfx::Point(75, 75));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_TRUE(
EvalJs(primary_main_frame_host(), "window.__clicked").ExtractBool());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, InputEventRouting) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('mousemove', (e) => {
window.__mouse_x = e.clientX;
window.__mouse_y = e.clientY;
});
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
SimulateMouseEvent(web_contents(), blink::WebInputEvent::Type::kMouseMove,
gfx::Point(50, 50));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ(50, EvalJs(primary_main_frame_host(), "window.__mouse_x"));
EXPECT_EQ(50, EvalJs(primary_main_frame_host(), "window.__mouse_y"));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, InputEventRoutingTouch) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('touchstart', (e) => {
window.__touch_x = e.touches[0].clientX;
window.__touch_y = e.touches[0].clientY;
});
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
std::unique_ptr<SyntheticPointerDriver> synthetic_pointer_driver =
SyntheticPointerDriver::Create(
content::mojom::GestureSourceType::kTouchInput,
/*from_devtools_debugger=*/true);
RenderWidgetHostImpl* render_widget_host =
primary_main_frame_host()->GetRenderWidgetHost();
auto* root_view = render_widget_host->GetView()->GetRootView();
std::unique_ptr<SyntheticGestureTarget> synthetic_gesture_target =
root_view ? root_view->CreateSyntheticGestureTarget()
: render_widget_host->GetView()->CreateSyntheticGestureTarget();
synthetic_pointer_driver->Press(50, 50, 0,
SyntheticPointerActionParams::Button::LEFT);
synthetic_pointer_driver->DispatchEvent(synthetic_gesture_target.get(),
base::TimeTicks::Now());
RunUntilInputProcessed(render_widget_host);
EXPECT_EQ(50, EvalJs(primary_main_frame_host(), "window.__touch_x"));
EXPECT_EQ(50, EvalJs(primary_main_frame_host(), "window.__touch_y"));
}
// TODO(crbug.com/534380085): Flaky/failing on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_LightDismissEscKey DISABLED_LightDismissEscKey
#else
#define MAYBE_LightDismissEscKey LightDismissEscKey
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, MAYBE_LightDismissEscKey) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
std::string get_style =
"getComputedStyle(document.getElementById('target')).visibility";
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
auto tracker = CreateDestructionTracker(*window);
SimulateKeyPress(web_contents(), ui::DomKey::ESCAPE, ui::DomCode::ESCAPE,
ui::VKEY_ESCAPE, false, false, false, false);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ("hidden", EvalJs(primary_main_frame_host(), get_style));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, LightDismissClickOutside) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:50px; height:50px;" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
std::string get_style =
"getComputedStyle(document.getElementById('target')).visibility";
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
auto tracker = CreateDestructionTracker(*window);
SimulateMouseClickAt(web_contents(), 0, blink::WebMouseEvent::Button::kLeft,
gfx::Point(300, 300));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ("hidden", EvalJs(primary_main_frame_host(), get_style));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
LightDismissEscKeyCanceledByBeforeToggle) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:50px; height:50px;" unbounded></div>
`;
const target = document.getElementById('target');
target.addEventListener('beforetoggle', e => {
if (e.cancelable && e.newState === 'closed') {
e.preventDefault();
}
});
target.showUnboundedElement();
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
std::string get_style =
"getComputedStyle(document.getElementById('target')).visibility";
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
SimulateKeyPress(web_contents(), ui::DomKey::ESCAPE, ui::DomCode::ESCAPE,
ui::VKEY_ESCAPE, false, false, false, false);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// Window should remain active and element visible because beforetoggle was
// canceled.
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
EXPECT_EQ(window, GetActiveWindow());
auto tracker = CreateDestructionTracker(*window);
EXPECT_TRUE(
ExecJs(primary_main_frame_host(),
"document.getElementById('target').hideUnboundedElement();"));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
LightDismissClickOutsideCanceledByBeforeToggle) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:50px; height:50px;" unbounded></div>
`;
const target = document.getElementById('target');
target.addEventListener('beforetoggle', e => {
if (e.cancelable && e.newState === 'closed') {
e.preventDefault();
}
});
target.showUnboundedElement();
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
std::string get_style =
"getComputedStyle(document.getElementById('target')).visibility";
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
SimulateMouseClickAt(web_contents(), 0, blink::WebMouseEvent::Button::kLeft,
gfx::Point(300, 300));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// Window should remain active and element visible because beforetoggle was
// canceled.
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), get_style));
EXPECT_EQ(window, GetActiveWindow());
auto tracker = CreateDestructionTracker(*window);
EXPECT_TRUE(
ExecJs(primary_main_frame_host(),
"document.getElementById('target').hideUnboundedElement();"));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, PopoverInsideUnbounded) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" unbounded>
<div id="popover" popover>Nested Popover</div>
</div>
`;
document.getElementById('child').showUnboundedElement().then(() => {
document.getElementById('popover').showPopover();
});
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
EXPECT_TRUE(EvalJs(primary_main_frame_host(),
"document.getElementById('popover')"
".matches(':popover-open')")
.ExtractBool());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, CompositorPopupAllocation) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:100px; height:100px;" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(100, bounds.width());
EXPECT_EQ(100, bounds.height());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, VisualOverflowBounds) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:100px; height:100px;
filter:drop-shadow(50px 50px 0px green);" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(150, bounds.width());
EXPECT_EQ(150, bounds.height());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
RequestWithZeroSizeBoundsSucceeds) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Execute script that calls showUnboundedElement on an element with zero size
// and verify it resolves successfully with a minimum 1x1 window allocation.
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:0; height:0;" unbounded></div>
`;
document.getElementById('target').showUnboundedElement()
.then(() => "Success", e => e.name);
)";
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(1, bounds.width());
EXPECT_EQ(1, bounds.height());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
RequestWithoutAttributeThrowsException) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Execute script that calls showUnboundedElement on an element
// without the 'unbounded' attribute and catches the exception name.
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target"></div>
`;
document.getElementById('target').showUnboundedElement()
.then(() => "Success", e => e.name);
)";
EXPECT_EQ("InvalidStateError", EvalJs(primary_main_frame_host(), script));
}
class UnboundedElementHighDPIBrowserTest : public UnboundedElementBrowserTest {
public:
UnboundedElementHighDPIBrowserTest() = default;
~UnboundedElementHighDPIBrowserTest() override = default;
void SetUp() override {
EnablePixelOutput(2.0f);
UnboundedElementBrowserTest::SetUp();
}
};
IN_PROC_BROWSER_TEST_P(UnboundedElementHighDPIBrowserTest,
CompositorPopupAllocationHighDPI) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" style="width:100px; height:100px;" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
float dsf = primary_main_frame_host()
->GetRenderWidgetHost()
->GetView()
->GetDeviceScaleFactor();
EXPECT_EQ(2.0f, dsf);
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(100, bounds.width());
EXPECT_EQ(100, bounds.height());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
VisualOverflowBoundsAndMasking) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
#child {
width: 200px;
height: 90px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
</style>
<div id="child" unbounded>
<div class="item">Content</div>
</div>
`;
document.getElementById('child').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
EXPECT_EQ("visible", EvalJs(primary_main_frame_host(), R"(
window.getComputedStyle(document.querySelector('.item')).visibility;
)"));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
gfx::Rect popup_bounds = window->GetBounds();
EXPECT_GE(popup_bounds.width(), 200);
EXPECT_GE(popup_bounds.height(), 90);
}
// Mouse events are not routed through UnboundedSurfaceWindow on Android, as
// native touch/pointer events are handled by the regular Android View
// hierarchy.
// TODO(crbug.com/508672616): Not yet working on ChromeOS due to Aura/Ash
// popup container positioning and coordinate conversion issues.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PopupInputEventRouting DISABLED_PopupInputEventRouting
#else
#define MAYBE_PopupInputEventRouting PopupInputEventRouting
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_PopupInputEventRouting) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('mousemove', (e) => {
window.__mouse_x = e.clientX;
window.__mouse_y = e.clientY;
});
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
blink::WebMouseEvent event(blink::WebInputEvent::Type::kMouseMove,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
gfx::Rect popup_bounds = window->GetBounds();
const int kMouseOffsetX = 50;
const int kMouseOffsetY = 50;
event.SetPositionInWidget(kMouseOffsetX, kMouseOffsetY);
event.SetPositionInScreen(popup_bounds.x() + kMouseOffsetX,
popup_bounds.y() + kMouseOffsetY);
window->RouteMouseEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ(kMouseOffsetX,
EvalJs(primary_main_frame_host(), "window.__mouse_x"));
EXPECT_EQ(kMouseOffsetY,
EvalJs(primary_main_frame_host(), "window.__mouse_y"));
}
// Mouse events are not routed through UnboundedSurfaceWindow on Android, as
// native touch/pointer events are handled by the regular Android View
// hierarchy.
// TODO(crbug.com/508672616): Not yet working on ChromeOS due to Aura/Ash
// popup container positioning and coordinate conversion issues.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PopupOutsideViewportInputEventRouting \
DISABLED_PopupOutsideViewportInputEventRouting
#else
#define MAYBE_PopupOutsideViewportInputEventRouting \
PopupOutsideViewportInputEventRouting
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_PopupOutsideViewportInputEventRouting) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
const int kOutsideElementLeft = 50;
const int kOutsideElementTop = 400;
std::string script = base::StringPrintf(
R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px; position:absolute;
top:%dpx; left:%dpx;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('mousemove', (e) => {
window.__mouse_x = e.clientX;
window.__mouse_y = e.clientY;
});
div.showUnboundedElement();
)",
kOutsideElementTop, kOutsideElementLeft);
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
blink::WebMouseEvent event(blink::WebInputEvent::Type::kMouseMove,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
gfx::Rect popup_bounds = window->GetBounds();
const int kMouseOffsetX = 50;
const int kMouseOffsetY = 70;
event.SetPositionInWidget(kMouseOffsetX, kMouseOffsetY);
event.SetPositionInScreen(popup_bounds.x() + kMouseOffsetX,
popup_bounds.y() + kMouseOffsetY);
window->RouteMouseEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// The expected document coordinates are calculated as:
// element_coordinate + mouse_offset_inside_popup.
constexpr int kExpectedMouseX = kOutsideElementLeft + kMouseOffsetX;
constexpr int kExpectedMouseY = kOutsideElementTop + kMouseOffsetY;
EXPECT_EQ(kExpectedMouseX,
EvalJs(primary_main_frame_host(), "window.__mouse_x"));
EXPECT_EQ(kExpectedMouseY,
EvalJs(primary_main_frame_host(), "window.__mouse_y"));
}
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_PopupOutsideViewportMouseWheelEventRouting \
DISABLED_PopupOutsideViewportMouseWheelEventRouting
#else
#define MAYBE_PopupOutsideViewportMouseWheelEventRouting \
PopupOutsideViewportMouseWheelEventRouting
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_PopupOutsideViewportMouseWheelEventRouting) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
const int kOutsideElementLeft = 50;
const int kOutsideElementTop = 400;
std::string script = base::StringPrintf(
R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px; position:absolute;
top:%dpx; left:%dpx;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('wheel', (e) => {
window.__wheel_x = e.clientX;
window.__wheel_y = e.clientY;
window.__wheel_delta_y = e.deltaY;
});
div.showUnboundedElement();
)",
kOutsideElementTop, kOutsideElementLeft);
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
blink::WebMouseWheelEvent event(blink::WebInputEvent::Type::kMouseWheel,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
gfx::Rect popup_bounds = window->GetBounds();
const int kMouseOffsetX = 50;
const int kMouseOffsetY = 70;
event.SetPositionInWidget(kMouseOffsetX, kMouseOffsetY);
event.SetPositionInScreen(popup_bounds.x() + kMouseOffsetX,
popup_bounds.y() + kMouseOffsetY);
event.delta_y = -50.0f;
event.wheel_ticks_y = -1.0f;
event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
window->RouteMouseWheelEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
constexpr int kExpectedMouseX = kOutsideElementLeft + kMouseOffsetX;
constexpr int kExpectedMouseY = kOutsideElementTop + kMouseOffsetY;
EXPECT_EQ(kExpectedMouseX,
EvalJs(primary_main_frame_host(), "window.__wheel_x"));
EXPECT_EQ(kExpectedMouseY,
EvalJs(primary_main_frame_host(), "window.__wheel_y"));
EXPECT_GT(EvalJs(primary_main_frame_host(), "window.__wheel_delta_y")
.ExtractDouble(),
0.0);
}
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_ScrollerOutsideViewportMouseWheelScroll \
DISABLED_ScrollerOutsideViewportMouseWheelScroll
#else
#define MAYBE_ScrollerOutsideViewportMouseWheelScroll \
ScrollerOutsideViewportMouseWheelScroll
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementEscapingOpenWebBrowserTest,
MAYBE_ScrollerOutsideViewportMouseWheelScroll) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
const int kOutsideElementLeft = 50;
const int kOutsideElementTop = 800;
std::string script = base::StringPrintf(
R"(
document.body.style.margin = '0';
document.body.style.height = '3000px';
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="scroller" style="width:200px; height:150px; position:absolute;
top:%dpx; left:%dpx; overflow-y:scroll;" unbounded>
<div style="height:1000px;">Scroller content</div>
</div>
`;
const scroller = document.getElementById('scroller');
scroller.showUnboundedElement();
)",
kOutsideElementTop, kOutsideElementLeft);
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
blink::WebMouseWheelEvent event(blink::WebInputEvent::Type::kMouseWheel,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
gfx::Rect popup_bounds = window->GetBounds();
const int kMouseOffsetX = 50;
const int kMouseOffsetY = 50;
event.SetPositionInWidget(kMouseOffsetX, kMouseOffsetY);
event.SetPositionInScreen(popup_bounds.x() + kMouseOffsetX,
popup_bounds.y() + kMouseOffsetY);
event.delta_y = -50.0f;
event.wheel_ticks_y = -1.0f;
event.phase = blink::WebMouseWheelEvent::kPhaseBegan;
window->RouteMouseWheelEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_TRUE(base::test::RunUntil([&]() {
return EvalJs(primary_main_frame_host(),
"document.getElementById('scroller').scrollTop")
.ExtractDouble() > 0.0;
}));
EXPECT_EQ(0, EvalJs(primary_main_frame_host(), "window.scrollY"));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
InputEventRoutingWithScroll) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.style.margin = '0';
document.body.style.height = '2000px';
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px; position:absolute;
top:400px; left:50px;" unbounded></div>
`;
window.scrollTo(0, 100);
const div = document.getElementById('child');
div.addEventListener('mousemove', (e) => {
window.__mouse_x = e.clientX;
window.__mouse_y = e.clientY;
});
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
// Element is at document (50, 400).
// Scroll is 100 down.
// Element is visible at viewport (50, 300).
// Simulate mouse move at viewport (100, 370) which is offset (50, 70) inside
// the element.
SimulateMouseEvent(web_contents(), blink::WebInputEvent::Type::kMouseMove,
gfx::Point(100, 370));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// We expect the event to be received with client coordinates matching the
// simulation.
EXPECT_EQ(100, EvalJs(primary_main_frame_host(), "window.__mouse_x"));
EXPECT_EQ(370, EvalJs(primary_main_frame_host(), "window.__mouse_y"));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
HoverOutsideBrowserWindowRendering) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// The browser window size is typically 800x600.
// Place an unbounded menu with two items outside the window:
// btn1 at y: 700..750 and btn2 at y: 800..850.
std::string script = R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<style>
#menu {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 1000px;
}
.item {
display: block;
width: 100px;
height: 50px;
border: none;
padding: 0;
margin: 0;
background-color: rgb(0, 0, 255);
}
.item:hover {
background-color: rgb(0, 255, 0);
}
</style>
<div id="menu" unbounded>
<div id="btn1" class="item" style="position:absolute; top:700px;"></div>
<div id="btn2" class="item" style="position:absolute; top:800px;"></div>
</div>
`;
document.getElementById('menu').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
// 1. Move mouse to btn1 (y: 725, outside viewport) to establish baseline
// outside viewport.
gfx::Rect popup_bounds = window->GetBounds();
{
blink::WebMouseEvent event(blink::WebInputEvent::Type::kMouseMove,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
event.SetPositionInWidget(50, 725);
event.SetPositionInScreen(popup_bounds.x() + 50, popup_bounds.y() + 725);
window->RouteMouseEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
}
// Force an animation frame so any lingering root damage from the initial
// transition settles.
std::ignore = EvalJs(primary_main_frame_host(),
"new Promise(r => requestAnimationFrame(() => "
"requestAnimationFrame(r)))");
// 2. Now move mouse from btn1 (y: 725) to btn2 (y: 825).
// Both btn1 and btn2 are outside the 800x600 window.
{
blink::WebMouseEvent event(blink::WebInputEvent::Type::kMouseMove,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
event.SetPositionInWidget(50, 825);
event.SetPositionInScreen(popup_bounds.x() + 50, popup_bounds.y() + 825);
window->RouteMouseEvent(event);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
}
// Verify that Blink JS computes the hover state as green on btn2.
EXPECT_EQ("rgb(0, 255, 0)",
EvalJs(primary_main_frame_host(),
"getComputedStyle(document.getElementById('btn2'))."
"backgroundColor"));
EXPECT_EQ("rgb(0, 0, 255)",
EvalJs(primary_main_frame_host(),
"getComputedStyle(document.getElementById('btn1'))."
"backgroundColor"));
}
// Mouse events are not routed through UnboundedSurfaceWindow on Android, as
// native touch/pointer events are handled by the regular Android View
// hierarchy.
// TODO(crbug.com/508672616): Not yet working on ChromeOS due to Aura/Ash
// popup container positioning and coordinate conversion issues.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_IframeClickEventRouting DISABLED_IframeClickEventRouting
#else
#define MAYBE_IframeClickEventRouting IframeClickEventRouting
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_IframeClickEventRouting) {
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Position and style the iframe.
std::string setup_script =
"document.getElementById('test_iframe').style.cssText = "
"'width:100px; height:100px; border:none; margin:0; position:absolute; "
"top:50px; left:50px;';";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
// Set up the unbounded element inside the iframe with a button extending
// outside the iframe's 100x100 bounds.
std::string iframe_script = R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<div id="child" style="width:50px; height:50px; position:absolute;
top:120px; left:120px;" unbounded>
<button id="btn" style="width:50px; height:50px;">Click</button>
</div>
`;
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
window.__clicked = true;
});
document.getElementById('child').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, iframe_script));
WaitForFrameReady();
// The iframe is at document (50, 50). Its bounds are [50, 50] to [150, 150].
// The child element is at iframe-document (120, 120), which is document (170,
// 170). Simulate mouse click at viewport (180, 180) which is offset (10, 10)
// inside the child button, and completely outside the iframe bounds.
SimulateMouseClickAt(web_contents(), 0, blink::WebMouseEvent::Button::kLeft,
gfx::Point(180, 180));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_TRUE(EvalJs(iframe, "window.__clicked").ExtractBool());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, IframeInputEventRouting) {
GURL url(embedded_test_server()->GetURL("/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
// Position and style the iframe.
std::string setup_script =
"document.getElementById('test_iframe').style.cssText = "
"'width:100px; height:100px; border:none; margin:0; position:absolute; "
"top:50px; left:50px;';";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
// Set up the unbounded element inside the iframe.
std::string iframe_script = R"(
document.body.style.margin = '0';
document.body.innerHTML = `
<div id="child" style="width:50px; height:50px; position:absolute;
top:120px; left:120px;" unbounded></div>
`;
const div = document.getElementById('child');
div.addEventListener('mousemove', (e) => {
window.__mouse_x = e.clientX;
window.__mouse_y = e.clientY;
});
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, iframe_script));
WaitForFrameReady();
// The iframe is at document (50, 50). Its bounds are [50, 50] to [150, 150].
// The child element is at iframe-document (120, 120), which is document (170,
// 170). Simulate mouse move at viewport (180, 180) which is offset (10, 10)
// inside the child element, and completely outside the iframe bounds.
SimulateMouseEvent(web_contents(), blink::WebInputEvent::Type::kMouseMove,
gfx::Point(180, 180));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// We expect the event to be received by the iframe with client coordinates
// matching the simulation.
EXPECT_EQ(130, EvalJs(iframe, "window.__mouse_x"));
EXPECT_EQ(130, EvalJs(iframe, "window.__mouse_y"));
}
// Disabled on Android because multi-window input event routing is not
// supported.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SubframeInputEventRouting DISABLED_SubframeInputEventRouting
#else
#define MAYBE_SubframeInputEventRouting SubframeInputEventRouting
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_SubframeInputEventRouting) {
GURL url(embedded_test_server()->GetURL("a.com", "/unbounded_subframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
WaitForHitTestData(iframe);
RenderWidgetHostViewBase* child_view =
static_cast<RenderFrameHostImpl*>(iframe)
->GetRenderWidgetHost()
->GetView();
ASSERT_TRUE(child_view);
EXPECT_TRUE(child_view->IsRenderWidgetHostViewChildFrame());
EXPECT_TRUE(ExecJs(iframe, "window.openUnbounded()"));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
MainThreadFrameObserver frame_observer(iframe->GetRenderWidgetHost());
frame_observer.Wait();
EXPECT_EQ(window->GetTargetView(), child_view);
RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
gfx::Point root_origin = root_view->GetViewBounds().origin();
double btn_center_x =
EvalJs(iframe,
"(() => {"
" const r = "
"document.getElementById('flyout_btn').getBoundingClientRect();"
" return r.left + r.width / 2;"
"})()")
.ExtractDouble();
double btn_center_y =
EvalJs(iframe,
"(() => {"
" const r = "
"document.getElementById('flyout_btn').getBoundingClientRect();"
" return r.top + r.height / 2;"
"})()")
.ExtractDouble();
gfx::PointF btn_center_in_child(btn_center_x, btn_center_y);
gfx::PointF btn_center_in_root =
child_view->TransformPointToRootCoordSpaceF(btn_center_in_child);
gfx::Point screen_point = gfx::ToRoundedPoint(
gfx::PointF(root_origin) + btn_center_in_root.OffsetFromOrigin());
// Route mouse move to the unbounded surface window.
blink::WebMouseEvent move_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseMove, screen_point.x(),
screen_point.y(), 0);
move_event.button = blink::WebMouseEvent::Button::kNoButton;
move_event.SetPositionInScreen(screen_point.x(), screen_point.y());
window->RouteMouseEvent(move_event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
EXPECT_EQ(btn_center_x, EvalJs(iframe, "window.__mouse_x").ExtractDouble());
EXPECT_EQ(btn_center_y, EvalJs(iframe, "window.__mouse_y").ExtractDouble());
// Route mouse click to the unbounded surface window.
blink::WebMouseEvent down_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseDown, screen_point.x(),
screen_point.y(), blink::WebInputEvent::kLeftButtonDown);
down_event.button = blink::WebMouseEvent::Button::kLeft;
down_event.click_count = 1;
down_event.SetPositionInScreen(screen_point.x(), screen_point.y());
window->RouteMouseEvent(down_event);
blink::WebMouseEvent up_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseUp, screen_point.x(), screen_point.y(),
0);
up_event.button = blink::WebMouseEvent::Button::kLeft;
up_event.click_count = 1;
up_event.SetPositionInScreen(screen_point.x(), screen_point.y());
window->RouteMouseEvent(up_event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
EXPECT_TRUE(EvalJs(iframe, "window.__clicked").ExtractBool());
}
// Verifies that when an unbounded element is positioned outside the subframe's
// `DocumentRect()` (because `overflow: hidden` clamps `DocumentRect()` to the
// 100x100 viewport), hit tests with `HitTestRequest::kIgnoreClipping` (such as
// mouse moves during active mouse capture) still use `InfiniteIntRect()` and
// reach the unbounded element.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SubframeInputEventRoutingIgnoreClipping \
DISABLED_SubframeInputEventRoutingIgnoreClipping
#else
#define MAYBE_SubframeInputEventRoutingIgnoreClipping \
SubframeInputEventRoutingIgnoreClipping
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_SubframeInputEventRoutingIgnoreClipping) {
GURL url(embedded_test_server()->GetURL(
"a.com", GetParam() ? "/unbounded_subframe.html"
: "/unbounded_subframe.html?same_origin"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
WaitForHitTestData(iframe);
// Clamp the iframe's `DocumentRect()` to its 100x100 viewport so the
// unbounded element at (120, 120) lies outside `DocumentRect()`.
EXPECT_TRUE(ExecJs(
iframe,
"document.documentElement.style.cssText = 'width:100%; height:100%; "
"overflow:hidden;';"
"document.body.style.cssText = 'margin:0; width:100%; height:100%; "
"overflow:hidden;';"));
RenderWidgetHostViewBase* child_view =
static_cast<RenderFrameHostImpl*>(iframe)
->GetRenderWidgetHost()
->GetView();
ASSERT_TRUE(child_view);
EXPECT_TRUE(ExecJs(iframe, "window.openUnbounded()"));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
MainThreadFrameObserver frame_observer(iframe->GetRenderWidgetHost());
frame_observer.Wait();
gfx::Rect popup_bounds = window->GetBounds();
gfx::Point screen_point = popup_bounds.CenterPoint();
double btn_center_x =
EvalJs(iframe,
"(() => {"
" const r = "
"document.getElementById('flyout_btn').getBoundingClientRect();"
" return r.left + r.width / 2;"
"})()")
.ExtractDouble();
double btn_center_y =
EvalJs(iframe,
"(() => {"
" const r = "
"document.getElementById('flyout_btn').getBoundingClientRect();"
" return r.top + r.height / 2;"
"})()")
.ExtractDouble();
// Press left mouse button down to initiate mouse capture, then send a
// dragged mouse move (`kLeftButtonDown`), which triggers hit testing with
// `HitTestRequest::kIgnoreClipping`.
blink::WebMouseEvent down_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseDown, screen_point.x(),
screen_point.y(), blink::WebInputEvent::kLeftButtonDown);
down_event.button = blink::WebMouseEvent::Button::kLeft;
down_event.click_count = 1;
down_event.SetPositionInScreen(screen_point.x(), screen_point.y());
window->RouteMouseEvent(down_event);
blink::WebMouseEvent drag_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseMove, screen_point.x() + 5,
screen_point.y() + 5, blink::WebInputEvent::kLeftButtonDown);
drag_event.button = blink::WebMouseEvent::Button::kLeft;
drag_event.SetPositionInScreen(screen_point.x() + 5, screen_point.y() + 5);
window->RouteMouseEvent(drag_event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
EXPECT_EQ(btn_center_x + 5,
EvalJs(iframe, "window.__mouse_x").ExtractDouble());
EXPECT_EQ(btn_center_y + 5,
EvalJs(iframe, "window.__mouse_y").ExtractDouble());
blink::WebMouseEvent up_event = blink::SyntheticWebMouseEventBuilder::Build(
blink::WebInputEvent::Type::kMouseUp, screen_point.x() + 5,
screen_point.y() + 5, 0);
up_event.button = blink::WebMouseEvent::Button::kLeft;
up_event.click_count = 1;
up_event.SetPositionInScreen(screen_point.x() + 5, screen_point.y() + 5);
window->RouteMouseEvent(up_event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
EXPECT_TRUE(EvalJs(iframe, "window.__clicked").ExtractBool());
}
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_SubframeScrollableUnboundedElement \
DISABLED_SubframeScrollableUnboundedElement
#else
#define MAYBE_SubframeScrollableUnboundedElement \
SubframeScrollableUnboundedElement
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_SubframeScrollableUnboundedElement) {
if (!GetParam()) {
// Same-origin subframes share the root view and compositor. Scrolling over
// parent-level occluders is a compositor scroll hit-testing limitation
// tracked separately (see go/same-origin-iframe-unbounded);
// RenderWidgetHostViewChildFrame is only exercised in OOPIF mode.
GTEST_SKIP();
}
GURL url(embedded_test_server()->GetURL(
"a.com", GetParam() ? "/unbounded_subframe.html"
: "/unbounded_subframe.html?same_origin"));
EXPECT_TRUE(NavigateToURL(shell(), url));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
EXPECT_TRUE(WaitForLoadStop(web_contents()));
WaitForHitTestData(iframe);
std::string script = R"(
document.body.innerHTML = `
<div id="scroller" style="width:150px; height:100px; position:absolute;
top:120px; left:120px; overflow-y:scroll;" unbounded>
<div style="height:600px;">Scroller content</div>
</div>
`;
document.getElementById('scroller').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, script));
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
MainThreadFrameObserver frame_observer(iframe->GetRenderWidgetHost());
frame_observer.Wait();
blink::WebMouseWheelEvent event(blink::WebInputEvent::Type::kMouseWheel,
blink::WebInputEvent::kNoModifiers,
base::TimeTicks::Now());
event.button = blink::WebMouseEvent::Button::kNoButton;
gfx::Rect popup_bounds = window->GetBounds();
const int kMouseOffsetX = 25;
const int kMouseOffsetY = 25;
event.SetPositionInWidget(kMouseOffsetX, kMouseOffsetY);
event.SetPositionInScreen(popup_bounds.x() + kMouseOffsetX,
popup_bounds.y() + kMouseOffsetY);
event.delta_y = -50.0f;
event.wheel_ticks_y = -1.0f;
window->RouteMouseWheelEvent(event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
double initial_scroll = 0.0;
EXPECT_TRUE(base::test::RunUntil([&]() {
initial_scroll =
EvalJs(iframe, "document.getElementById('scroller').scrollTop")
.ExtractDouble();
return initial_scroll > 0.0;
}));
// Route a second wheel tick to verify multi-tick wheel scrolling.
event.SetTimeStamp(base::TimeTicks::Now());
window->RouteMouseWheelEvent(event);
RunUntilInputProcessed(iframe->GetRenderWidgetHost());
EXPECT_TRUE(base::test::RunUntil([&]() {
return EvalJs(iframe, "document.getElementById('scroller').scrollTop")
.ExtractDouble() > initial_scroll;
}));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, DynamicBoundsSync) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="child" style="width:100px; height:100px; position:absolute;
top:0; left:0;" unbounded></div>
`;
const div = document.getElementById('child');
div.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window =
primary_main_frame_host()->GetUnboundedSurfaceWindow();
ASSERT_TRUE(window);
// Verify initial bounds
{
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(100, bounds.width());
EXPECT_EQ(100, bounds.height());
}
// Update style properties to trigger bounds update
std::string update_script = R"(
const div = document.getElementById('child');
div.style.width = '150px';
div.style.height = '200px';
div.style.left = '50px';
div.style.top = '50px';
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), update_script));
// Allow layout and pre-paint to propagate the new bounds to the browser
std::ignore = EvalJs(primary_main_frame_host(),
"new Promise(r => requestAnimationFrame(() => "
"requestAnimationFrame(r)))");
// Verify updated bounds
{
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(150, bounds.width());
EXPECT_EQ(200, bounds.height());
}
}
// TODO(crbug.com/534380085): Flaky/failing on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_NestedChildBoundsExpansionTriggersRedraw \
DISABLED_NestedChildBoundsExpansionTriggersRedraw
#else
#define MAYBE_NestedChildBoundsExpansionTriggersRedraw \
NestedChildBoundsExpansionTriggersRedraw
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_NestedChildBoundsExpansionTriggersRedraw) {
#if BUILDFLAG(IS_OZONE)
if (ui::OzonePlatform::RunningOnWaylandForTest()) {
// TODO(crbug.com/523970924): Flaky/failing on
// linux-wayland-mutter-rel-tests.
GTEST_SKIP();
}
#endif
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<div id="parent" style="display:flex; width:100px; height:100px;"
unbounded>
<div style="width:100px; height:100px; flex-shrink:0;"></div>
<div id="child" style="width:100px; height:100px; flex-shrink:0;
display:none;"></div>
</div>
`;
const parent = document.getElementById('parent');
parent.showUnboundedElement().then(() => "OK", e => e.name);
)";
EXPECT_EQ("OK", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window =
primary_main_frame_host()->GetUnboundedSurfaceWindow();
ASSERT_TRUE(window);
// Initial bounds should only be parent (100x100)
{
gfx::Rect bounds = window->GetBounds();
EXPECT_EQ(100, bounds.width());
EXPECT_EQ(100, bounds.height());
}
// Show child
std::string update_script = R"(
document.getElementById('child').style.display = 'block';
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), update_script));
std::ignore = EvalJs(primary_main_frame_host(),
"new Promise(r => requestAnimationFrame(() => "
"requestAnimationFrame(r)))");
EXPECT_EQ(200, window->GetBounds().width());
}
// TODO(crbug.com/534380085): Flaky/failing on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_AnimatedChildWithBoxShadowSubmitsFrame \
DISABLED_AnimatedChildWithBoxShadowSubmitsFrame
#else
#define MAYBE_AnimatedChildWithBoxShadowSubmitsFrame \
AnimatedChildWithBoxShadowSubmitsFrame
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MAYBE_AnimatedChildWithBoxShadowSubmitsFrame) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.submenu {
display: none;
width: 100px;
height: 100px;
}
.submenu.is-open {
display: block;
animation: fadeIn 0.2s ease-in-out;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.22);
}
</style>
<div id="parent" style="display:flex; width:100px; height:100px;"
unbounded>
<div style="width:100px; height:100px;"></div>
<div id="child" class="submenu"></div>
</div>
`;
const parent = document.getElementById('parent');
parent.showUnboundedElement().then(() => "OK", e => e.name);
)";
EXPECT_EQ("OK", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window =
primary_main_frame_host()->GetUnboundedSurfaceWindow();
ASSERT_TRUE(window);
std::string update_script = R"(
document.getElementById('child').classList.add('is-open');
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), update_script));
EXPECT_TRUE(base::test::RunUntil(
[&]() { return window->GetBounds().width() > 100; }));
// Force subsequent animation frame under the same LocalSurfaceId
std::ignore = EvalJs(primary_main_frame_host(),
"new Promise(r => requestAnimationFrame(() => "
"requestAnimationFrame(r)))");
base::test::TestFuture<const content::CopyFromSurfaceResult&> future_result;
window->CopyFromSurface(gfx::Rect(), window->GetBounds().size(),
base::TimeDelta(), future_result.GetCallback());
auto result = future_result.Take();
EXPECT_TRUE(result.has_value());
if (result.has_value()) {
EXPECT_FALSE(result.value().bitmap.drawsNothing());
}
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
IframeDeletionDoesNotDismissUnboundedSurface) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" unbounded></div>
<iframe id="test_iframe" src="about:blank"></iframe>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
EXPECT_TRUE(window->IsValid());
// Remove the iframe and verify it doesn't dismiss the unbounded surface.
EXPECT_TRUE(ExecJs(primary_main_frame_host(),
"document.getElementById('test_iframe').remove();"));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_TRUE(window->IsValid());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
DoesNotStealFocusWhenOpened) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="c">
<input id="i">
</div>
`;
const i = document.getElementById('i');
const c = document.getElementById('c');
i.focus();
c.setAttribute('unbounded', '');
c.showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
EXPECT_TRUE(window->IsValid());
SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('a'),
ui::DomCode::US_A, ui::VKEY_A, false, false, false, false);
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ("a", EvalJs(primary_main_frame_host(),
"document.getElementById('i').value"));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest, CloseOnWindowFocusLost) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
ASSERT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
EXPECT_TRUE(window->IsValid());
auto tracker = CreateDestructionTracker(*window);
// Simulate the browser window losing focus.
primary_main_frame_host()->GetRenderWidgetHost()->Blur();
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
std::string get_style =
"getComputedStyle(document.getElementById('target')).visibility";
EXPECT_EQ("hidden", EvalJs(primary_main_frame_host(), get_style));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
MultipleUnboundedElementsDismissesFirst) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"JS(
document.body.innerHTML = `
<meta name="viewport" content="width=device-width, initial-scale=1">
<div id="first" unbounded></div>
<div id="second" unbounded></div>
`;
const first = document.getElementById('first');
const second = document.getElementById('second');
let results = [];
first.showUnboundedElement()
.then(() => {
results.push(getComputedStyle(first).visibility);
results.push(getComputedStyle(second).visibility);
return second.showUnboundedElement();
})
.then(() => {
results.push(getComputedStyle(first).visibility);
results.push(getComputedStyle(second).visibility);
return results.join(',');
}, err => "error:" + err);
)JS";
EXPECT_EQ("visible,hidden,hidden,visible",
EvalJs(primary_main_frame_host(), script).ExtractString());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
DisplayNoneDismissesSurface) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<div id="wrapper" unbounded
style="display:flex; gap:20px; width:max-content;">
<div id="m1" style="width:100px; height:100px; background:red;">
Menu1
</div>
<div id="m2" style="width:100px; height:100px; background:blue;">
Menu2
</div>
</div>
`;
const wrapper = document.getElementById('wrapper');
wrapper.showUnboundedElement().then(() => "OK", e => e.name);
)";
EXPECT_EQ("OK", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window =
primary_main_frame_host()->GetUnboundedSurfaceWindow();
ASSERT_TRUE(window);
auto tracker = CreateDestructionTracker(*window);
EXPECT_TRUE(ExecJs(primary_main_frame_host(), R"(
const wrapper = document.getElementById('wrapper');
wrapper.style.display = 'none';
document.body.offsetHeight;
)"));
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
EXPECT_EQ(false,
EvalJs(primary_main_frame_host(),
"document.getElementById('wrapper').matches(':unbounded')"));
WaitForDestruction(std::move(tracker));
}
IN_PROC_BROWSER_TEST_P(
UnboundedElementEscapingOpenWebBrowserTest,
WindowResizeWithScrolledPageUpdatesBoundsInViewportCoordinates) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#spacer { height: 3000px; }
#target {
position: absolute;
top: 1000px;
left: 100px;
width: 100px;
height: 100px;
}
</style>
<div id="spacer"></div>
<div id="target" unbounded></div>
`;
window.scrollTo(0, 500);
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
gfx::Rect initial_bounds = window->GetBounds();
gfx::Vector2d initial_offset =
initial_bounds.origin() - view->GetViewBounds().origin();
EXPECT_EQ(100, initial_offset.x());
EXPECT_EQ(500, initial_offset.y());
EXPECT_EQ(100, initial_bounds.width());
EXPECT_EQ(100, initial_bounds.height());
// Trigger UpdateVisualProperties via browser window / view resize.
gfx::Size current_size = view->GetVisibleViewportSize();
view->SetSize(gfx::Size(current_size.width() == 800 ? 700 : 800,
current_size.height() == 600 ? 500 : 600));
WaitForFrameReady();
RunUntilInputProcessed(primary_main_frame_host()->GetRenderWidgetHost());
// Verify that after resize, the bounds are still in viewport coordinates
// (y=500), not raw document coordinates (y=1000).
gfx::Rect updated_bounds = window->GetBounds();
gfx::Vector2d updated_offset =
updated_bounds.origin() - view->GetViewBounds().origin();
EXPECT_EQ(100, updated_offset.x());
EXPECT_EQ(500, updated_offset.y());
EXPECT_EQ(100, updated_bounds.width());
EXPECT_EQ(100, updated_bounds.height());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
OpenWebConstrainedToViewport) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: -100px;
left: -100px;
width: 3000px;
height: 3000px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
EXPECT_TRUE(view->GetViewBounds().Contains(window->GetBounds()));
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
}
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
OpenWebIframeConstrainedToViewport) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL iframe_url(embedded_test_server()->GetURL("/title1.html"));
std::string setup_script = base::StringPrintf(R"(
const iframe = document.createElement('iframe');
iframe.id = 'subframe';
iframe.allow = 'unbounded-element';
iframe.src = '%s';
iframe.style.cssText = 'width:200px; height:200px; position:absolute; ' +
'top:50px; left:50px; border:none;';
document.body.appendChild(iframe);
)",
iframe_url.spec().c_str());
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
WaitForHitTestData(iframe);
std::string iframe_script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: -200px;
left: -200px;
width: 4000px;
height: 4000px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, iframe_script));
WaitForFrameReady();
MainThreadFrameObserver iframe_frame_observer(iframe->GetRenderWidgetHost());
iframe_frame_observer.Wait();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
EXPECT_TRUE(view->GetViewBounds().Contains(window->GetBounds()));
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
}
// Sends oversized bounds directly to the browser's UnboundedSurfaceHost
// implementation, bypassing the renderer-side clamp, and checks that the
// browser clamps them to the viewport.
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
BrowserClampsOversizedBoundsFromMainFrame) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: 10px;
left: 10px;
width: 50px;
height: 50px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
gfx::Rect expected_initial_bounds(10, 10, 50, 50);
expected_initial_bounds.Offset(view->GetViewBounds().OffsetFromOrigin());
EXPECT_EQ(expected_initial_bounds, window->GetBounds());
window->UpdateBounds(gfx::Rect(-1000, -1000, 10000, 10000));
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
}
// Same as OpenWebIframeConstrainedToViewport, but with a cross-site (OOPIF)
// iframe. The iframe's renderer can't clamp to the (remote) top frame, so the
// browser is responsible for the viewport clamp.
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
OpenWebCrossSiteIframeConstrainedToViewport) {
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL iframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
std::string setup_script = base::StringPrintf(R"(
const iframe = document.createElement('iframe');
iframe.id = 'subframe';
iframe.allow = 'unbounded-element';
iframe.src = '%s';
iframe.style.cssText = 'width:200px; height:200px; position:absolute; ' +
'top:50px; left:50px; border:none;';
document.body.appendChild(iframe);
)",
iframe_url.spec().c_str());
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
ASSERT_TRUE(iframe->IsCrossProcessSubframe());
WaitForHitTestData(iframe);
std::string iframe_script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: -200px;
left: -200px;
width: 4000px;
height: 4000px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, iframe_script));
WaitForFrameReady();
MainThreadFrameObserver iframe_frame_observer(iframe->GetRenderWidgetHost());
iframe_frame_observer.Wait();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
// Bypass the renderer entirely.
window->UpdateBounds(gfx::Rect(-1000, -1000, 10000, 10000));
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
}
// A cross-site (OOPIF) iframe without allow="unbounded-element" must be
// constrained to the iframe's bounds, including when oversized bounds are sent
// directly to the browser.
IN_PROC_BROWSER_TEST_P(UnboundedElementBrowserTest,
OpenWebCrossSiteIframeWithoutAllowConstrainedToIframe) {
GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
GURL iframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
std::string setup_script = base::StringPrintf(R"(
const iframe = document.createElement('iframe');
iframe.id = 'subframe';
iframe.src = '%s';
iframe.style.cssText = 'width:200px; height:200px; position:absolute; ' +
'top:50px; left:50px; border:none;';
document.body.appendChild(iframe);
)",
iframe_url.spec().c_str());
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
ASSERT_TRUE(iframe->IsCrossProcessSubframe());
WaitForHitTestData(iframe);
std::string iframe_script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: -200px;
left: -200px;
width: 4000px;
height: 4000px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement();
)";
EXPECT_TRUE(ExecJs(iframe, iframe_script));
WaitForFrameReady();
MainThreadFrameObserver iframe_frame_observer(iframe->GetRenderWidgetHost());
iframe_frame_observer.Wait();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
gfx::Rect expected_bounds(50, 50, 200, 200);
expected_bounds.Offset(view->GetViewBounds().OffsetFromOrigin());
EXPECT_EQ(expected_bounds, window->GetBounds());
// Bypass the renderer entirely.
window->UpdateBounds(gfx::Rect(-1000, -1000, 10000, 10000));
EXPECT_EQ(expected_bounds, window->GetBounds());
}
INSTANTIATE_TEST_SUITE_P(All, UnboundedElementBrowserTest, testing::Bool());
IN_PROC_BROWSER_TEST_P(UnboundedElementEscapingOpenWebBrowserTest,
OpenWebCanEscapeWithUnsafeEscapingFlag) {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
std::string script = R"(
document.body.style.margin = '0';
document.body.style.padding = '0';
const target = document.createElement('div');
target.id = 'target';
target.setAttribute('unbounded', '');
target.style.cssText =
'position: absolute; top: -50px; left: -50px; ' +
'width: 3000px; height: 3000px;';
document.body.appendChild(target);
target.showUnboundedElement().then(() => "Success", e => e.name);
)";
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
gfx::Rect expected_screen_bounds = gfx::Rect(-50, -50, 3000, 3000);
expected_screen_bounds.Offset(view->GetViewBounds().OffsetFromOrigin());
EXPECT_EQ(expected_screen_bounds, window->GetBounds());
EXPECT_FALSE(view->GetViewBounds().Contains(window->GetBounds()));
}
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
UnboundedElementEscapingOpenWebBrowserTest);
class UnboundedElementWebUITest : public UnboundedElementBrowserTestBase {
public:
UnboundedElementWebUITest() = default;
~UnboundedElementWebUITest() override = default;
void SetUp() override {
SkipTestsForUnsupportedPlatforms();
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kUnboundedElement},
/*disabled_features=*/{
blink::features::kUnsafeUnboundedElementOnTheOpenWeb,
blink::features::kUnsafeEscapingUnboundedElementOnTheOpenWeb,
::features::kTreesInViz});
UnboundedElementBrowserTestBase::SetUp();
}
void SetUpOnMainThread() override {
UnboundedElementBrowserTestBase::SetUpOnMainThread();
browser_client_ = std::make_unique<AllowSubframesBrowserClient>();
}
void TearDownOnMainThread() override {
browser_client_.reset();
UnboundedElementBrowserTestBase::TearDownOnMainThread();
}
private:
// TODO(crbug.com/571879162): Remove once the temporary allowlist is removed.
class AllowSubframesBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
bool IsUnboundedElementAllowedForSubframesOf(
const url::Origin& origin) override {
return true;
}
};
TestWebUIControllerFactory factory_;
ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<AllowSubframesBrowserClient> browser_client_;
};
IN_PROC_BROWSER_TEST_F(
UnboundedElementWebUITest,
UntrustedIframeAllowedWithPermissionsPolicyAndConstrainedToViewport) {
GURL webui_url(
GetWebUIURL("web-ui/title1.html?childsrc=*&notrustedtypes=true"));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
GURL iframe_url(embedded_test_server()->GetURL("/title1.html"));
std::string setup_script = base::StringPrintf(R"(
const iframe = document.createElement('iframe');
iframe.id = 'untrusted_iframe';
iframe.allow = 'unbounded-element';
iframe.src = '%s';
iframe.style.cssText = 'width:200px; height:200px; position:absolute; ' +
'top:50px; left:50px; border:none;';
document.body.appendChild(iframe);
)",
iframe_url.spec().c_str());
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
WaitForHitTestData(iframe);
std::string iframe_script = R"(
document.body.innerHTML = `
<style>
body { margin: 0; padding: 0; }
#target {
position: absolute;
top: -200px;
left: -200px;
width: 4000px;
height: 4000px;
}
</style>
<div id="target" unbounded></div>
`;
document.getElementById('target').showUnboundedElement().then(
() => "Success", e => e.name);
)";
EXPECT_EQ("Success", EvalJs(iframe, iframe_script));
WaitForFrameReady();
MainThreadFrameObserver iframe_frame_observer(iframe->GetRenderWidgetHost());
iframe_frame_observer.Wait();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
EXPECT_TRUE(view->GetViewBounds().Contains(window->GetBounds()));
EXPECT_EQ(view->GetViewBounds(), window->GetBounds());
}
IN_PROC_BROWSER_TEST_F(UnboundedElementWebUITest,
UntrustedIframeDeniedWithoutPermissionsPolicy) {
GURL webui_url(
GetWebUIURL("web-ui/title1.html?childsrc=*&notrustedtypes=true"));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
GURL iframe_url(embedded_test_server()->GetURL("/title1.html"));
std::string setup_script = base::StringPrintf(R"(
const iframe = document.createElement('iframe');
iframe.id = 'untrusted_iframe';
iframe.src = '%s';
iframe.style.cssText = 'width:200px; height:200px; position:absolute; ' +
'top:50px; left:50px; border:none;';
document.body.appendChild(iframe);
)",
iframe_url.spec().c_str());
EXPECT_TRUE(ExecJs(primary_main_frame_host(), setup_script));
EXPECT_TRUE(WaitForLoadStop(web_contents()));
RenderFrameHost* iframe = ChildFrameAt(primary_main_frame_host(), 0);
ASSERT_TRUE(iframe);
std::string check_script = R"(
const div = document.createElement('div');
'showUnboundedElement' in div ? "Present" : "Missing";
)";
EXPECT_EQ("Missing", EvalJs(iframe, check_script));
}
IN_PROC_BROWSER_TEST_F(UnboundedElementWebUITest,
WebUIMainFrameCanEscapeViewport) {
GURL webui_url(
GetWebUIURL("web-ui/title1.html?childsrc=*&notrustedtypes=true"));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
std::string script = R"(
document.body.style.margin = '0';
document.body.style.padding = '0';
const target = document.createElement('div');
target.id = 'target';
target.setAttribute('unbounded', '');
target.style.cssText =
'position: absolute; top: -50px; left: -50px; ' +
'width: 3000px; height: 3000px;';
document.body.appendChild(target);
target.showUnboundedElement().then(() => "Success", e => e.name);
)";
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), script));
WaitForFrameReady();
UnboundedSurfaceWindow* window = GetActiveWindow();
ASSERT_TRUE(window);
RenderWidgetHostViewBase* view = static_cast<RenderWidgetHostViewBase*>(
primary_main_frame_host()->GetRenderWidgetHost()->GetView());
ASSERT_TRUE(view);
gfx::Rect expected_screen_bounds = gfx::Rect(-50, -50, 3000, 3000);
expected_screen_bounds.Offset(view->GetViewBounds().OffsetFromOrigin());
EXPECT_EQ(expected_screen_bounds, window->GetBounds());
EXPECT_FALSE(view->GetViewBounds().Contains(window->GetBounds()));
}
INSTANTIATE_TEST_SUITE_P(All,
UnboundedElementHighDPIBrowserTest,
testing::Bool());
struct UnboundedElementPermutationTestParams {
bool unbounded_element_base_feature;
bool unbounded_element_runtime_feature;
bool open_web_base_feature;
bool open_web_runtime_feature;
bool is_privileged;
};
class UnboundedElementPermutationBrowserTest
: public UnboundedElementBrowserTestBase,
public ::testing::WithParamInterface<
UnboundedElementPermutationTestParams> {
public:
UnboundedElementPermutationBrowserTest() = default;
~UnboundedElementPermutationBrowserTest() override = default;
void SetUp() override {
SkipTestsForUnsupportedPlatforms();
const auto& params = GetParam();
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
if (params.unbounded_element_base_feature) {
enabled_features.push_back(blink::features::kUnboundedElement);
} else {
disabled_features.push_back(blink::features::kUnboundedElement);
}
if (params.open_web_base_feature) {
enabled_features.push_back(
blink::features::kUnsafeUnboundedElementOnTheOpenWeb);
} else {
disabled_features.push_back(
blink::features::kUnsafeUnboundedElementOnTheOpenWeb);
}
disabled_features.push_back(::features::kTreesInViz);
feature_list_.InitWithFeatures(enabled_features, disabled_features);
UnboundedElementBrowserTestBase::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
UnboundedElementBrowserTestBase::SetUpCommandLine(command_line);
const auto& params = GetParam();
std::vector<std::string> enabled_blink_features;
std::vector<std::string> disabled_blink_features;
if (params.unbounded_element_runtime_feature ||
params.open_web_runtime_feature) {
enabled_blink_features.push_back("UnboundedElement");
} else {
disabled_blink_features.push_back("UnboundedElement");
}
if (params.open_web_runtime_feature) {
enabled_blink_features.push_back("UnsafeUnboundedElementOnTheOpenWeb");
} else {
disabled_blink_features.push_back("UnsafeUnboundedElementOnTheOpenWeb");
}
if (!enabled_blink_features.empty()) {
command_line->AppendSwitchASCII(
switches::kEnableBlinkFeatures,
base::JoinString(enabled_blink_features, ","));
}
if (!disabled_blink_features.empty()) {
command_line->AppendSwitchASCII(
switches::kDisableBlinkFeatures,
base::JoinString(disabled_blink_features, ","));
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
#if BUILDFLAG(IS_IOS)
#define MAYBE_CheckPermutation DISABLED_CheckPermutation
#else
#define MAYBE_CheckPermutation CheckPermutation
#endif
IN_PROC_BROWSER_TEST_P(UnboundedElementPermutationBrowserTest,
MAYBE_CheckPermutation) {
const auto& params = GetParam();
if (params.is_privileged) {
GURL webui_url(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
EXPECT_TRUE(NavigateToURL(shell(), webui_url));
} else {
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), url));
}
// Blink's UnboundedElement runtime feature is implied if
// UnsafeUnboundedElementOnTheOpenWeb runtime feature is enabled.
bool effective_unbounded_runtime = params.unbounded_element_runtime_feature ||
params.open_web_runtime_feature;
// The JS API (showUnboundedElement) is exposed in Blink renderer if:
// 1. Base feature kUnboundedElement is enabled.
// 2. Effective UnboundedElement runtime feature is enabled.
// 3. AND at least one of (is_privileged, open_web_base_feature) is true.
bool expected_api_exposed =
params.unbounded_element_base_feature && effective_unbounded_runtime &&
(params.is_privileged || params.open_web_base_feature);
std::string check_api_script = R"(
const div = document.createElement('div');
'showUnboundedElement' in div ? "Present" : "Missing";
)";
EXPECT_EQ(expected_api_exposed ? "Present" : "Missing",
EvalJs(primary_main_frame_host(), check_api_script,
EXECUTE_SCRIPT_NO_USER_GESTURE));
if (expected_api_exposed) {
std::string invoke_script = R"(
const div = document.createElement('div');
div.setAttribute('unbounded', '');
document.body.appendChild(div);
div.showUnboundedElement().then(() => "Success", e => e.name);
)";
bool expected_browser_allowed =
params.unbounded_element_base_feature &&
(params.is_privileged || params.open_web_base_feature);
if (params.is_privileged) {
// Privileged pages bypass user activation.
if (expected_browser_allowed) {
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), invoke_script,
EXECUTE_SCRIPT_NO_USER_GESTURE));
} else {
ScopedAllowRendererCrashes scoped_allow_renderer_crashes(
primary_main_frame_host()->GetProcess());
RenderProcessHostBadMojoMessageWaiter kill_waiter(
primary_main_frame_host()->GetProcess());
ExecuteScriptAsync(primary_main_frame_host(), invoke_script);
EXPECT_TRUE(kill_waiter.Wait().has_value());
}
} else {
// Non-privileged pages reject in JS with NotAllowedError when user
// activation is missing.
EXPECT_EQ("NotAllowedError",
EvalJs(primary_main_frame_host(), invoke_script,
EXECUTE_SCRIPT_NO_USER_GESTURE));
// With user activation:
if (expected_browser_allowed) {
EXPECT_EQ("Success", EvalJs(primary_main_frame_host(), invoke_script));
} else {
ScopedAllowRendererCrashes scoped_allow_renderer_crashes(
primary_main_frame_host()->GetProcess());
RenderProcessHostBadMojoMessageWaiter kill_waiter(
primary_main_frame_host()->GetProcess());
ExecuteScriptAsync(primary_main_frame_host(), invoke_script);
EXPECT_TRUE(kill_waiter.Wait().has_value());
}
}
}
}
namespace {
std::vector<UnboundedElementPermutationTestParams> GeneratePermutations() {
std::vector<UnboundedElementPermutationTestParams> params;
for (bool unbounded_base : {false, true}) {
for (bool unbounded_runtime : {false, true}) {
for (bool open_web_base : {false, true}) {
for (bool open_web_runtime : {false, true}) {
for (bool is_privileged : {false, true}) {
params.push_back({unbounded_base, unbounded_runtime, open_web_base,
open_web_runtime, is_privileged});
}
}
}
}
}
return params;
}
} // namespace
INSTANTIATE_TEST_SUITE_P(
All,
UnboundedElementPermutationBrowserTest,
testing::ValuesIn(GeneratePermutations()),
[](const testing::TestParamInfo<UnboundedElementPermutationTestParams>&
info) {
return base::StringPrintf(
"UnboundedBase%s_UnboundedRuntime%s_OpenWebBase%s_OpenWebRuntime%s_"
"%s",
info.param.unbounded_element_base_feature ? "On" : "Off",
info.param.unbounded_element_runtime_feature ? "On" : "Off",
info.param.open_web_base_feature ? "On" : "Off",
info.param.open_web_runtime_feature ? "On" : "Off",
info.param.is_privileged ? "Privileged" : "OpenWeb");
});
} // namespace content