| // Copyright 2023 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/test/scoped_feature_list.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/browser/ui/exclusive_access/exclusive_access_manager.h" |
| #include "chrome/browser/ui/exclusive_access/fullscreen_controller.h" |
| #include "chrome/browser/ui/test/popup_test_base.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/rect.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 popups with multi-screen features from the Window Management API. |
| // Tests are run with and without the requisite Window Management permission. |
| // Tests must run in series to manage virtual displays on supported platforms. |
| // Use 2+ physical displays to run locally with --gtest_also_run_disabled_tests. |
| #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PopupMultiScreenTest PopupMultiScreenTest |
| #else |
| #define MAYBE_PopupMultiScreenTest DISABLED_PopupMultiScreenTest |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_MAC) |
| class MAYBE_PopupMultiScreenTest : public PopupTestBase, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| MAYBE_PopupMultiScreenTest() { |
| scoped_feature_list_.InitWithFeatures( |
| {blink::features::kFullscreenPopupWindows}, {}); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| PopupTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitch(switches::kIgnoreCertificateErrors); |
| } |
| |
| void SetUpOnMainThread() override { |
| if (!SetUpVirtualDisplays()) { |
| GTEST_SKIP() << "Virtual displays not supported on this platform."; |
| } |
| ASSERT_GE(display::Screen::GetScreen()->GetNumDisplays(), 2); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| embedded_test_server()->GetURL("/simple.html"))); |
| EXPECT_TRUE(WaitForRenderFrameReady(web_contents->GetPrimaryMainFrame())); |
| if (ShouldTestWindowManagement()) { |
| SetUpWindowManagement(browser()); |
| } |
| } |
| |
| void TearDownOnMainThread() override { |
| #if BUILDFLAG(IS_MAC) |
| virtual_display_util_.reset(); |
| #endif |
| } |
| |
| protected: |
| bool ShouldTestWindowManagement() { return GetParam(); } |
| |
| // Create virtual displays as needed, ensuring 2 displays are available for |
| // testing multi-screen functionality. Not all platforms and OS versions are |
| // supported. Returns false if virtual displays could not be 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-802x803"); |
| 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); |
| return true; |
| } |
| return false; |
| #else |
| return false; |
| #endif |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| #if BUILDFLAG(IS_MAC) |
| std::unique_ptr<display::test::VirtualDisplayMacUtil> virtual_display_util_; |
| #endif |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(, MAYBE_PopupMultiScreenTest, ::testing::Bool()); |
| |
| // Tests opening a popup without explicit bounds. |
| IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, Basic) { |
| // Copy the display vector so references are not invalidated while looping. |
| std::vector<display::Display> displays = |
| display::Screen::GetScreen()->GetAllDisplays(); |
| for (const display::Display& opener_display : displays) { |
| browser()->window()->SetBounds(opener_display.work_area()); |
| ASSERT_EQ(opener_display, GetDisplayNearestBrowser(browser())); |
| for (const char* url : {"/simple.html", "about:blank"}) { |
| const std::string open_script = |
| content::JsReplace("open($1, '', 'popup');", url); |
| Browser* popup = OpenPopup(browser(), open_script); |
| display::Display popup_display = GetDisplayNearestBrowser(popup); |
| // The popup should open on the same screen as the opener. |
| EXPECT_EQ(opener_display.id(), popup_display.id()) |
| << " expected: " << opener_display.work_area().ToString() |
| << " actual: " << popup_display.work_area().ToString() |
| << " popup: " << popup->window()->GetBounds().ToString() |
| << " script: " << open_script; |
| // The popup is constrained to the available bounds of its screen. |
| const gfx::Rect popup_bounds = popup->window()->GetBounds(); |
| EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds)) |
| << " work_area: " << popup_display.work_area().ToString() |
| << " popup: " << popup_bounds.ToString(); |
| } |
| } |
| } |
| |
| // Tests opening a popup on another screen. |
| IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, OpenOnAnotherScreen) { |
| // Copy the display vector so references are not invalidated while looping. |
| std::vector<display::Display> displays = |
| display::Screen::GetScreen()->GetAllDisplays(); |
| for (const display::Display& opener_display : displays) { |
| browser()->window()->SetBounds(opener_display.work_area()); |
| ASSERT_EQ(opener_display, GetDisplayNearestBrowser(browser())); |
| for (const display::Display& target_display : displays) { |
| for (const char* url : {"/simple.html", "about:blank"}) { |
| const std::string open_script = content::JsReplace( |
| "open($1, '', 'left=$2,top=$3,width=200,height=200');", url, |
| target_display.work_area().x(), target_display.work_area().y()); |
| Browser* popup = OpenPopup(browser(), open_script); |
| display::Display popup_display = GetDisplayNearestBrowser(popup); |
| // The popup only opens on another screen with permission. |
| const display::Display& expected_display = |
| ShouldTestWindowManagement() ? target_display : opener_display; |
| EXPECT_EQ(expected_display.id(), popup_display.id()) |
| << " expected: " << expected_display.work_area().ToString() |
| << " actual: " << popup_display.work_area().ToString() |
| << " opener: " << browser()->window()->GetBounds().ToString() |
| << " popup: " << popup->window()->GetBounds().ToString() |
| << " script: " << open_script; |
| // The popup is constrained to the available bounds of its screen. |
| const gfx::Rect popup_bounds = popup->window()->GetBounds(); |
| EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds)) |
| << " work_area: " << popup_display.work_area().ToString() |
| << " popup: " << popup_bounds.ToString(); |
| } |
| } |
| } |
| } |
| |
| // Tests opening a popup on the same screen, then moving it to another screen. |
| IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, MoveToAnotherScreen) { |
| content::WebContents* opener_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| // Copy the display vector so references are not invalidated while looping. |
| display::Screen* screen = display::Screen::GetScreen(); |
| std::vector<display::Display> displays = screen->GetAllDisplays(); |
| for (const display::Display& opener_display : displays) { |
| browser()->window()->SetBounds(opener_display.work_area()); |
| ASSERT_EQ(opener_display, GetDisplayNearestBrowser(browser())); |
| gfx::Point opener_display_center = opener_display.work_area().CenterPoint(); |
| for (const display::Display& target_display : displays) { |
| for (const char* url : {"/simple.html", "about:blank"}) { |
| const std::string open_script = content::JsReplace( |
| "w = open($1, '', 'left=$2,top=$3,width=200,height=200');", url, |
| opener_display_center.x() - 100, opener_display_center.y() - 100); |
| Browser* popup = OpenPopup(browser(), open_script); |
| EXPECT_EQ(opener_display, GetDisplayNearestBrowser(popup)); |
| // Ensure the opener can access the popup window object. |
| ASSERT_NE("", EvalJs(opener_contents, "w.location.href")); |
| |
| // Have the opener try to move the popup to the target screen. |
| const std::string move_script = content::JsReplace( |
| "w.moveTo($1, $2);", target_display.work_area().x(), |
| target_display.work_area().y()); |
| { |
| // TODO(crbug.com/1444721): Resolve any WaitForBoundsChange flakes. |
| auto log = [](const std::vector<display::Display>& displays) { |
| std::string log; |
| for (const display::Display& d : displays) { |
| log += " " + d.ToString() + "\n"; |
| } |
| return log; |
| }; |
| content::RenderFrameHost* opener_rfh = |
| opener_contents->GetPrimaryMainFrame(); |
| content::WebContents* popup_contents = |
| popup->tab_strip_model()->GetActiveWebContents(); |
| content::RenderFrameHost* popup_rfh = |
| popup_contents->GetPrimaryMainFrame(); |
| SCOPED_TRACE( |
| testing::Message() |
| << "\n" |
| << "script: " << open_script << " " << move_script |
| << "\n" |
| // Opener details: |
| << "opener bounds: " |
| << browser()->window()->GetBounds().ToString() |
| << " GetVisibleURL: " |
| << opener_contents->GetVisibleURL().possibly_invalid_spec() |
| << " GetLastCommittedURL: " |
| << opener_rfh->GetLastCommittedURL().possibly_invalid_spec() |
| << " GetLastCommittedOrigin: " |
| << opener_rfh->GetLastCommittedOrigin().GetDebugString() |
| << " IsInPrimaryMainFrame: " << opener_rfh->IsInPrimaryMainFrame() |
| << " GetLifecycleState: " |
| << static_cast<std::underlying_type< |
| content::RenderFrameHost::LifecycleState>::type>( |
| opener_rfh->GetLifecycleState()) |
| << " IsActive: " << opener_rfh->IsActive() |
| << " IsDOMContentLoaded: " << opener_rfh->IsDOMContentLoaded() |
| << " IsFeatureEnabled: " |
| << opener_rfh->IsFeatureEnabled( |
| blink::mojom::PermissionsPolicyFeature::kWindowManagement) |
| << "\n" |
| // Popup details: |
| << "popup bounds: " << popup->window()->GetBounds().ToString() |
| << " GetVisibleURL: " |
| << popup_contents->GetVisibleURL().possibly_invalid_spec() |
| << " GetLastCommittedURL: " |
| << popup_rfh->GetLastCommittedURL().possibly_invalid_spec() |
| << " GetLastCommittedOrigin: " |
| << popup_rfh->GetLastCommittedOrigin().GetDebugString() |
| << " IsInPrimaryMainFrame: " << popup_rfh->IsInPrimaryMainFrame() |
| << " GetLifecycleState: " |
| << static_cast<std::underlying_type< |
| content::RenderFrameHost::LifecycleState>::type>( |
| popup_rfh->GetLifecycleState()) |
| << " IsActive: " << popup_rfh->IsActive() |
| << " IsDOMContentLoaded: " << popup_rfh->IsDOMContentLoaded() |
| << " IsFeatureEnabled: " |
| << popup_rfh->IsFeatureEnabled( |
| blink::mojom::PermissionsPolicyFeature::kWindowManagement) |
| << "\n" |
| // Display details: |
| << "cached displays:\n" |
| << log(displays) << "current displays:\n" |
| << log(screen->GetAllDisplays())); |
| ASSERT_TRUE(content::WaitForLoadStop(opener_contents)); |
| ASSERT_TRUE(content::WaitForRenderFrameReady(opener_rfh)); |
| ASSERT_EQ("complete", EvalJs(opener_contents, "document.readyState")); |
| ASSERT_EQ("visible", |
| EvalJs(opener_contents, "document.visibilityState")); |
| ASSERT_TRUE(content::WaitForLoadStop(popup_contents)); |
| ASSERT_TRUE(content::WaitForRenderFrameReady(popup_rfh)); |
| ASSERT_EQ("complete", EvalJs(popup_contents, "document.readyState")); |
| ASSERT_EQ("visible", |
| EvalJs(popup_contents, "document.visibilityState")); |
| content::ExecuteScriptAsync(opener_contents, move_script); |
| WaitForBoundsChange(popup, /*move_by=*/40, /*resize_by=*/0); |
| } |
| const display::Display popup_display = GetDisplayNearestBrowser(popup); |
| |
| // The popup only moves to another screen with permission. |
| const display::Display& expected_display = |
| ShouldTestWindowManagement() ? target_display : opener_display; |
| EXPECT_EQ(expected_display.id(), popup_display.id()) |
| << " expected: " << expected_display.work_area().ToString() |
| << " actual: " << popup_display.work_area().ToString() |
| << " opener: " << browser()->window()->GetBounds().ToString() |
| << " popup: " << popup->window()->GetBounds().ToString() |
| << " script: " << open_script << " " << move_script; |
| // The popup is constrained to the available bounds of its screen. |
| const gfx::Rect popup_bounds = popup->window()->GetBounds(); |
| EXPECT_TRUE(popup_display.work_area().Contains(popup_bounds)) |
| << " work_area: " << popup_display.work_area().ToString() |
| << " popup: " << popup_bounds.ToString(); |
| } |
| } |
| } |
| } |
| |
| // Tests opening a popup on another screen from a cross-origin iframe. |
| IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, CrossOriginIFrame) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK); |
| https_server.AddDefaultHandlers(GetChromeTestDataDir()); |
| content::SetupCrossSiteRedirector(&https_server); |
| ASSERT_TRUE(https_server.Start()); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, |
| https_server.GetURL("a.com", "/simple.html"))); |
| EXPECT_TRUE(WaitForRenderFrameReady(web_contents->GetPrimaryMainFrame())); |
| // Grant permission to the new origin after navigation. |
| if (ShouldTestWindowManagement()) { |
| SetUpWindowManagement(browser()); |
| } |
| |
| // Append cross-origin iframes with and without the permission policy. |
| const GURL src = https_server.GetURL("b.com", "/simple.html"); |
| const std::string script = R"JS( |
| new Promise(resolve => { |
| let f = document.createElement('iframe'); |
| f.src = $1; |
| f.allow = $2 ? 'window-management' : ''; |
| f.addEventListener('load', () => resolve(true)); |
| document.body.appendChild(f); |
| }); |
| )JS"; |
| EXPECT_EQ(true, EvalJs(web_contents, content::JsReplace(script, src, false))); |
| EXPECT_EQ(true, EvalJs(web_contents, content::JsReplace(script, src, true))); |
| |
| // Copy the display vector so references are not invalidated while looping. |
| std::vector<display::Display> displays = |
| display::Screen::GetScreen()->GetAllDisplays(); |
| for (const display::Display& opener_display : displays) { |
| browser()->window()->SetBounds(opener_display.work_area()); |
| ASSERT_EQ(opener_display, GetDisplayNearestBrowser(browser())); |
| for (const bool iframe_policy_granted : {true, false}) { |
| content::RenderFrameHost* cross_origin_iframe = |
| ChildFrameAt(web_contents, iframe_policy_granted ? 1 : 0); |
| ASSERT_TRUE(cross_origin_iframe); |
| ASSERT_NE(cross_origin_iframe->GetLastCommittedOrigin(), |
| web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| for (const display::Display& target_display : displays) { |
| for (const char* url : {"/simple.html", "about:blank"}) { |
| const std::string open_script = content::JsReplace( |
| "w = open($1, '', 'left=$2,top=$3,width=200,height=200');", url, |
| target_display.work_area().x(), target_display.work_area().y()); |
| Browser* popup = OpenPopup(cross_origin_iframe, open_script); |
| display::Display popup_display = GetDisplayNearestBrowser(popup); |
| // The popup only opens on another screen with permission. |
| const display::Display& expected_display = |
| ShouldTestWindowManagement() && iframe_policy_granted |
| ? target_display |
| : opener_display; |
| EXPECT_EQ(expected_display.id(), popup_display.id()) |
| << " expected: " << expected_display.work_area().ToString() |
| << " actual: " << popup_display.work_area().ToString() |
| << " opener: " << browser()->window()->GetBounds().ToString() |
| << " popup: " << popup->window()->GetBounds().ToString() |
| << " script: " << open_script; |
| } |
| } |
| } |
| } |
| } |
| |
| // Tests opening a fullscreen popup on another display, when permitted. |
| IN_PROC_BROWSER_TEST_P(MAYBE_PopupMultiScreenTest, FullscreenDifferentScreen) { |
| // Falls back to opening a popup on the current screen in testing scenarios |
| // where window management is not granted in SetUpWindowManagement(). |
| Browser* popup = OpenPopup(browser(), R"JS( |
| (() => |
| { |
| otherScreen = (!!window.screenDetails && screenDetails.screens |
| .find(s => s != screenDetails.currentScreen)) || window.screen; |
| return open('/simple.html', '_blank', |
| `top=${otherScreen.availTop}, |
| left=${otherScreen.availLeft}, |
| height=200, |
| width=200, |
| popup, |
| fullscreen`); |
| })() |
| )JS"); |
| |
| content::WebContents* popup_contents = |
| popup->tab_strip_model()->GetActiveWebContents(); |
| if (ShouldTestWindowManagement()) { |
| WaitForHTMLFullscreen(popup_contents); |
| } |
| EXPECT_EQ(EvalJs(popup_contents, |
| "!!document.fullscreenElement && " |
| "document.fullscreenElement == document.documentElement") |
| .ExtractBool(), |
| ShouldTestWindowManagement()); |
| EXPECT_TRUE(EvalJs(popup_contents, |
| "screen.availLeft == opener.otherScreen.availLeft && " |
| "screen.availTop == opener.otherScreen.availTop") |
| .ExtractBool()); |
| FullscreenController* fullscreen_controller = |
| popup->exclusive_access_manager()->fullscreen_controller(); |
| EXPECT_FALSE(fullscreen_controller->IsFullscreenForBrowser()); |
| EXPECT_EQ(fullscreen_controller->IsTabFullscreen(), |
| ShouldTestWindowManagement()); |
| } |
| |
| } // namespace |