blob: 2724fedbf09db3833de86cefa93ac02fe700d0fd [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/permissions/permission_request_manager.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/screen_base.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/shell.h"
#include "ui/display/test/display_manager_test_api.h" // nogncheck
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_MAC)
#include "ui/display/mac/test/virtual_display_mac_util.h"
#endif // BUILDFLAG(IS_MAC)
namespace {
// Tests of window placement for popup browser windows.
class PopupBrowserTest : public InProcessBrowserTest {
public:
PopupBrowserTest() = default;
PopupBrowserTest(const PopupBrowserTest&) = delete;
PopupBrowserTest& operator=(const PopupBrowserTest&) = delete;
protected:
~PopupBrowserTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(embedder_support::kDisablePopupBlocking);
}
display::Display GetDisplayNearestBrowser(const Browser* browser) const {
return display::Screen::GetScreen()->GetDisplayNearestWindow(
browser->window()->GetNativeWindow());
}
Browser* OpenPopup(Browser* browser, const std::string& script) const {
auto* contents = browser->tab_strip_model()->GetActiveWebContents();
content::ExecuteScriptAsync(contents, script);
Browser* popup = ui_test_utils::WaitForBrowserToOpen();
EXPECT_NE(popup, browser);
auto* popup_contents = popup->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(WaitForRenderFrameReady(popup_contents->GetPrimaryMainFrame()));
return popup;
}
};
// A helper class to wait for widget bounds changes beyond given thresholds.
class WidgetBoundsChangeWaiter final : public views::WidgetObserver {
public:
WidgetBoundsChangeWaiter(views::Widget* widget, int move_by, int resize_by)
: widget_(widget),
move_by_(move_by),
resize_by_(resize_by),
initial_bounds_(widget->GetWindowBoundsInScreen()) {
widget_->AddObserver(this);
}
WidgetBoundsChangeWaiter(const WidgetBoundsChangeWaiter&) = delete;
WidgetBoundsChangeWaiter& operator=(const WidgetBoundsChangeWaiter&) = delete;
~WidgetBoundsChangeWaiter() final { widget_->RemoveObserver(this); }
// views::WidgetObserver:
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& rect) final {
if (BoundsChangeMeetsThreshold(rect)) {
widget_->RemoveObserver(this);
run_loop_.Quit();
}
}
// Wait for changes to occur, or return immediately if they already have.
void Wait() {
if (!BoundsChangeMeetsThreshold(widget_->GetWindowBoundsInScreen())) {
run_loop_.Run();
}
}
private:
bool BoundsChangeMeetsThreshold(const gfx::Rect& rect) const {
return (std::abs(rect.x() - initial_bounds_.x()) >= move_by_ ||
std::abs(rect.y() - initial_bounds_.y()) >= move_by_) &&
(std::abs(rect.width() - initial_bounds_.width()) >= resize_by_ ||
std::abs(rect.height() - initial_bounds_.height()) >= resize_by_);
}
const raw_ptr<views::Widget> widget_;
const int move_by_, resize_by_;
const gfx::Rect initial_bounds_;
base::RunLoop run_loop_;
};
// A helper class to wait for the bounds of two widgets to become equal.
class WidgetBoundsEqualWaiter final : public views::WidgetObserver {
public:
WidgetBoundsEqualWaiter(views::Widget* widget, views::Widget* widget_cmp)
: widget_(widget), widget_cmp_(widget_cmp) {
widget_->AddObserver(this);
widget_cmp_->AddObserver(this);
}
WidgetBoundsEqualWaiter(const WidgetBoundsEqualWaiter&) = delete;
WidgetBoundsEqualWaiter& operator=(const WidgetBoundsEqualWaiter&) = delete;
~WidgetBoundsEqualWaiter() final {
widget_->RemoveObserver(this);
widget_cmp_->RemoveObserver(this);
}
// views::WidgetObserver:
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& rect) final {
if (WidgetsBoundsEqual()) {
widget_->RemoveObserver(this);
widget_cmp_->RemoveObserver(this);
run_loop_.Quit();
}
}
// Wait for changes to occur, or return immediately if they already have.
void Wait() {
if (!WidgetsBoundsEqual()) {
run_loop_.Run();
}
}
private:
bool WidgetsBoundsEqual() {
return widget_->GetWindowBoundsInScreen() ==
widget_cmp_->GetWindowBoundsInScreen();
}
const raw_ptr<views::Widget> widget_ = nullptr;
const raw_ptr<views::Widget> widget_cmp_ = nullptr;
base::RunLoop run_loop_;
};
// Ensure `left=0,top=0` popup window feature coordinates are respected.
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, OpenLeftAndTopZeroCoordinates) {
// Attempt to open a popup at (0,0). Its bounds should match the request, but
// be adjusted to meet minimum size and available display area constraints.
Browser* popup =
OpenPopup(browser(), "open('.', '', 'left=0,top=0,width=50,height=50')");
const gfx::Rect work_area = GetDisplayNearestBrowser(popup).work_area();
gfx::Rect expected(popup->window()->GetBounds().size());
expected.AdjustToFit(work_area);
#if BUILDFLAG(IS_LINUX)
// TODO(crbug.com/1286870) Desktop Linux window bounds are inaccurate.
expected.Outset(50);
EXPECT_TRUE(expected.Contains(popup->window()->GetBounds()))
<< " expected: " << expected.ToString()
<< " popup: " << popup->window()->GetBounds().ToString()
<< " work_area: " << work_area.ToString();
#else
EXPECT_EQ(expected.ToString(), popup->window()->GetBounds().ToString())
<< " work_area: " << work_area.ToString();
#endif
}
// Ensure popups are opened in the available space of the opener's display.
// TODO(crbug.com/1211516): Flaky.
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, DISABLED_OpenClampedToCurrentDisplay) {
const auto display = GetDisplayNearestBrowser(browser());
EXPECT_TRUE(display.work_area().Contains(browser()->window()->GetBounds()))
<< "The browser window should be contained by its display's work area";
// Attempt to open a popup outside the bounds of the opener's display.
const char* const open_scripts[] = {
"open('.', '', 'left=' + (screen.availLeft - 50));",
"open('.', '', 'left=' + (screen.availLeft + screen.availWidth + 50));",
"open('.', '', 'top=' + (screen.availTop - 50));",
"open('.', '', 'top=' + (screen.availTop + screen.availHeight + 50));",
"open('.', '', 'left=' + (screen.availLeft - 50) + "
"',top=' + (screen.availTop - 50));",
"open('.', '', 'left=' + (screen.availLeft - 50) + "
"',top=' + (screen.availTop - 50) + "
"',width=300,height=300');",
"open('.', '', 'left=' + (screen.availLeft + screen.availWidth + 50) + "
"',top=' + (screen.availTop + screen.availHeight + 50) + "
"',width=300,height=300');",
"open('.', '', 'left=' + screen.availLeft + ',top=' + screen.availTop + "
"',width=' + (screen.availWidth + 300) + ',height=300');",
"open('.', '', 'left=' + screen.availLeft + ',top=' + screen.availTop + "
"',width=300,height='+ (screen.availHeight + 300));",
"open('.', '', 'left=' + screen.availLeft + ',top=' + screen.availTop + "
"',width=' + (screen.availWidth + 300) + "
"',height='+ (screen.availHeight + 300));",
};
for (auto* const script : open_scripts) {
Browser* popup = OpenPopup(browser(), script);
// The popup should be constrained to the opener's available display space.
// TODO(crbug.com/897300): Wait for the final window placement to occur;
// this is flakily checking initial or intermediate window placement bounds.
EXPECT_EQ(display, GetDisplayNearestBrowser(popup));
EXPECT_TRUE(display.work_area().Contains(popup->window()->GetBounds()))
<< " script: " << script
<< " work_area: " << display.work_area().ToString()
<< " popup: " << popup->window()->GetBounds().ToString();
}
}
// Ensure popups cannot be moved beyond the available display space by script.
// TODO(crbug.com/1228795): Flaking on Linux Ozone
#if BUILDFLAG(IS_LINUX) && BUILDFLAG(IS_OZONE)
#define MAYBE_MoveClampedToCurrentDisplay DISABLED_MoveClampedToCurrentDisplay
#else
#define MAYBE_MoveClampedToCurrentDisplay MoveClampedToCurrentDisplay
#endif
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, MAYBE_MoveClampedToCurrentDisplay) {
const auto display = GetDisplayNearestBrowser(browser());
const char kOpenPopup[] =
"open('.', '', 'left=' + (screen.availLeft + 50) + "
"',top=' + (screen.availTop + 50) + "
"',width=150,height=100');";
const char* const kMoveScripts[] = {
"moveBy(screen.availWidth * 2, 0);",
"moveBy(screen.availWidth * -2, 0);",
"moveBy(0, screen.availHeight * 2);",
"moveBy(0, screen.availHeight * -2);",
"moveBy(screen.availWidth * 2, screen.availHeight * 2);",
"moveBy(screen.availWidth * -2, screen.availHeight * -2);",
"moveTo(screen.availLeft + screen.availWidth + 50, screen.availTop);",
"moveTo(screen.availLeft - 50, screen.availTop);",
"moveTo(screen.availLeft, screen.availTop + screen.availHeight + 50);",
"moveTo(screen.availLeft, screen.availTop - 50);",
("moveTo(screen.availLeft + screen.availWidth + 50, "
"screen.availTop + screen.availHeight + 50);"),
"moveTo(screen.availLeft - 50, screen.availTop - 50);",
};
for (auto* const script : kMoveScripts) {
Browser* popup = OpenPopup(browser(), kOpenPopup);
auto popup_bounds = popup->window()->GetBounds();
auto* popup_contents = popup->tab_strip_model()->GetActiveWebContents();
auto* widget = views::Widget::GetWidgetForNativeWindow(
popup->window()->GetNativeWindow());
content::ExecuteScriptAsync(popup_contents, script);
// Wait for the substantial move, widgets may move during initialization.
WidgetBoundsChangeWaiter(widget, /*move_by=*/40, /*resize_by=*/0).Wait();
EXPECT_NE(popup_bounds.origin(), popup->window()->GetBounds().origin());
EXPECT_EQ(popup_bounds.size(), popup->window()->GetBounds().size());
EXPECT_TRUE(display.work_area().Contains(popup->window()->GetBounds()))
<< " script: " << script
<< " work_area: " << display.work_area().ToString()
<< " popup: " << popup_bounds.ToString();
}
}
// Ensure popups cannot be resized beyond the available display space by script.
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, ResizeClampedToCurrentDisplay) {
const auto display = GetDisplayNearestBrowser(browser());
const char kOpenPopup[] =
"open('.', '', 'left=' + (screen.availLeft + 50) + "
"',top=' + (screen.availTop + 50) + "
"',width=150,height=100');";
// The popup cannot be resized beyond the current screen by script.
const char* const kResizeScripts[] = {
"resizeBy(screen.availWidth * 2, 0);",
"resizeBy(0, screen.availHeight * 2);",
"resizeTo(screen.availWidth + 200, 200);",
"resizeTo(200, screen.availHeight + 200);",
"resizeTo(screen.availWidth + 200, screen.availHeight + 200);",
};
for (auto* const script : kResizeScripts) {
Browser* popup = OpenPopup(browser(), kOpenPopup);
auto popup_bounds = popup->window()->GetBounds();
auto* popup_contents = popup->tab_strip_model()->GetActiveWebContents();
auto* widget = views::Widget::GetWidgetForNativeWindow(
popup->window()->GetNativeWindow());
content::ExecuteScriptAsync(popup_contents, script);
// Wait for the substantial resize, widgets may move during initialization.
WidgetBoundsChangeWaiter(widget, /*move_by=*/0, /*resize_by=*/100).Wait();
EXPECT_NE(popup_bounds.size(), popup->window()->GetBounds().size());
EXPECT_TRUE(display.work_area().Contains(popup->window()->GetBounds()))
<< " script: " << script
<< " work_area: " << display.work_area().ToString()
<< " popup: " << popup_bounds.ToString();
}
}
// Opens two popups with custom position and size, but one has noopener. They
// should both have the same position and size. http://crbug.com/1011688
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, NoopenerPositioning) {
Browser* noopener_popup = OpenPopup(
browser(),
"open('.', '', 'noopener=1,height=200,width=200,top=100,left=100')");
Browser* opener_popup = OpenPopup(
browser(), "open('.', '', 'height=200,width=200,top=100,left=100')");
WidgetBoundsEqualWaiter(views::Widget::GetWidgetForNativeWindow(
noopener_popup->window()->GetNativeWindow()),
views::Widget::GetWidgetForNativeWindow(
opener_popup->window()->GetNativeWindow()))
.Wait();
EXPECT_EQ(noopener_popup->window()->GetBounds(),
opener_popup->window()->GetBounds());
}
// Tests popups with extended features from the Window Management API.
// Test fixtures are run with and without multi-screen Window Management
// permission. Manages virtual displays on supported platforms.
class WindowManagementPopupBrowserTest
: public PopupBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
void TearDownOnMainThread() override {
#if BUILDFLAG(IS_MAC)
virtual_display_util_.reset();
#endif
PopupBrowserTest::TearDownOnMainThread();
}
protected:
bool ShouldTestWindowManagement() { return GetParam(); }
// Requests screen details and grants window management permission.
void SetUpWindowManagement() {
if (!ShouldTestWindowManagement()) {
return;
}
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
// Request and auto-accept the permission request.
permissions::PermissionRequestManager* permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(contents);
permission_request_manager->set_auto_response_for_test(
permissions::PermissionRequestManager::ACCEPT_ALL);
ASSERT_GT(EvalJs(contents,
R"JS(getScreenDetails().then(s => {
window.screenDetails = s;
return s.screens.length; }))JS"),
0);
// Do not auto-accept any other permission requests.
permission_request_manager->set_auto_response_for_test(
permissions::PermissionRequestManager::NONE);
}
// Initializes the embedded test server and navigates to an empty page.
void SetUpWebServer() {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/empty.html")));
}
// Waits until an element is fullscreen in the specified web contents.
// Returns immediately if an element is already fullscreen.
void WaitForHTMLFullscreen(content::WebContents* contents) {
content::WaitForLoadStop(contents);
ASSERT_TRUE(EvalJs(contents, R"JS(
(new Promise(r => {
if (!!document.fullscreenElement) {
r();
} else {
document.addEventListener(`fullscreenchange`,
() => { if (!!document.fullscreenElement) r(); },
{once: true}
);
}
})))JS")
.error.empty());
}
// Attempts to create virtual displays such that 2 displays become available
// for testing multi-screen functionality. Not all platforms and OS versions
// are supported. Returns false if virtual displays could not be created.
// If the host already has 2 or more displays available, no virtual displays
// are created.
bool SetUpVirtualDisplays() {
if (display::Screen::GetScreen()->GetNumDisplays() > 1) {
return true;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.UpdateDisplay("100+100-801x802,901+100-802x802");
AssertMinimumDisplayCount(2);
return true;
#elif BUILDFLAG(IS_MAC)
if (display::test::VirtualDisplayMacUtil::IsAPIAvailable()) {
virtual_display_util_ =
std::make_unique<display::test::VirtualDisplayMacUtil>();
virtual_display_util_->AddDisplay(
1, display::test::VirtualDisplayMacUtil::k1920x1080);
AssertMinimumDisplayCount(2);
return true;
}
return false;
#else
return false;
#endif
}
// Asserts that the test environment has at least `count` screens available.
void AssertMinimumDisplayCount(int count) {
ASSERT_GE(count, display::Screen::GetScreen()->GetNumDisplays());
}
private:
#if BUILDFLAG(IS_MAC)
std::unique_ptr<display::test::VirtualDisplayMacUtil> virtual_display_util_;
#endif
};
INSTANTIATE_TEST_SUITE_P(All,
WindowManagementPopupBrowserTest,
::testing::Bool());
// TODO(crbug.com/1183791): Disabled everywhere except ChromeOS and Mac because
// of races with SetScreenInstance and observers not being notified.
#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_MAC)
#define MAYBE_AboutBlankCrossScreenPlacement AboutBlankCrossScreenPlacement
#else
#define MAYBE_AboutBlankCrossScreenPlacement \
DISABLED_AboutBlankCrossScreenPlacement
#endif
// Tests that an about:blank popup can be moved across screens with permission.
IN_PROC_BROWSER_TEST_P(WindowManagementPopupBrowserTest,
MAYBE_AboutBlankCrossScreenPlacement) {
#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_MAC)
if (!SetUpVirtualDisplays()) {
GTEST_SKIP() << "Virtual displays not supported on this platform.";
}
#else
display::ScreenBase test_screen;
test_screen.display_list().AddDisplay({1, gfx::Rect(100, 100, 801, 802)},
display::DisplayList::Type::PRIMARY);
test_screen.display_list().AddDisplay(
{2, gfx::Rect(901, 100, 802, 802)},
display::DisplayList::Type::NOT_PRIMARY);
display::Screen::SetScreenInstance(&test_screen);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
AssertMinimumDisplayCount(2);
SetUpWebServer();
auto* opener = browser()->tab_strip_model()->GetActiveWebContents();
// TODO(crbug.com/1119974): this test could be in content_browsertests
// and not browser_tests if permission controls were supported.
SetUpWindowManagement();
// Open an about:blank popup. It should start on the same screen as browser().
Browser* popup = OpenPopup(
browser(), "w = open('about:blank', '', 'width=200,height=200');");
const auto opener_display = GetDisplayNearestBrowser(browser());
auto original_popup_display = GetDisplayNearestBrowser(popup);
EXPECT_EQ(opener_display, original_popup_display);
const auto second_display = display::Screen::GetScreen()->GetAllDisplays()[1];
const std::string move_popup_to_the_second_screen_script = base::StringPrintf(
"w.moveTo(%d, %d);", second_display.work_area().x() + 100,
second_display.work_area().y() + 100);
// Have the opener try to move the popup to the second screen.
content::ExecuteScriptAsync(opener, move_popup_to_the_second_screen_script);
// Wait for the substantial move, widgets may move during initialization.
auto* widget = views::Widget::GetWidgetForNativeWindow(
popup->window()->GetNativeWindow());
WidgetBoundsChangeWaiter(widget, /*move_by=*/40, /*resize_by=*/0).Wait();
auto new_popup_display = GetDisplayNearestBrowser(popup);
// The popup only moves to the second screen with permission.
EXPECT_EQ(ShouldTestWindowManagement(),
original_popup_display != new_popup_display);
EXPECT_EQ(ShouldTestWindowManagement(), second_display == new_popup_display);
// The popup is always constrained to the bounds of the target display.
auto popup_bounds = popup->window()->GetBounds();
EXPECT_TRUE(new_popup_display.work_area().Contains(popup_bounds))
<< " work_area: " << new_popup_display.work_area().ToString()
<< " popup: " << popup_bounds.ToString();
#if !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_MAC)
display::Screen::SetScreenInstance(nullptr);
#endif // !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_MAC)
}
} // namespace