// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include "base/strings/string_util.h" #include "build/build_config.h" #include "content/browser/renderer_host/frame_tree_node.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/browser/host_zoom_map.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_types.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_utils.h" #include "content/public/test/test_navigation_observer.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" #include "net/dns/mock_host_resolver.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/page/page_zoom.h" #include "url/gurl.h" namespace content { // This class contains basic tests of zoom functionality. class ZoomBrowserTest : public ContentBrowserTest { public: ZoomBrowserTest() {} protected: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); SetupCrossSiteRedirector(embedded_test_server()); ASSERT_TRUE(embedded_test_server()->Start()); } WebContentsImpl* web_contents() { return static_cast(shell()->web_contents()); } }; // This class contains tests to make sure that subframes zoom in a manner // consistent with the top-level frame, even when the subframes are cross-site. // Particular things we want to make sure of: // // * Subframes should always have the same zoom level as their main frame, even // if the subframe's domain has a different zoom level stored in HostZoomMap. // // * The condition above should continue to hold after a navigation of the // subframe. // // * Zoom changes applied to the mainframe should propagate to all subframes, // regardless of whether they are same site or cross-site to the frame they are // children of. // // The tests in this file rely on the notion that, when a page zooms, that // subframes have both (1) a change in their frame rect, and (2) a change in // their frame's scale. Since the page should scale as a unit, this means the // innerWidth value of any subframe should be the same before and after the // zoom (though it may transiently take on a different value). The // FrameSizeObserver serves to watch for onresize events, and observes when // the innerWidth is correctly set. class IFrameZoomBrowserTest : public ContentBrowserTest { public: IFrameZoomBrowserTest() {} protected: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); SetupCrossSiteRedirector(embedded_test_server()); ASSERT_TRUE(embedded_test_server()->Start()); } WebContentsImpl* web_contents() { return static_cast(shell()->web_contents()); } }; namespace { const double kTolerance = 0.1; // In CSS pixels. double GetMainframeWindowBorder(const ToRenderFrameHost& adapter) { double border; const char kGetMainframeBorder[] = "window.domAutomationController.send(" "window.outerWidth - window.innerWidth" ");"; EXPECT_TRUE( ExecuteScriptAndExtractDouble(adapter, kGetMainframeBorder, &border)); return border; } double GetMainFrameZoomFactor(const ToRenderFrameHost& adapter, double border) { double zoom_factor; EXPECT_TRUE(ExecuteScriptAndExtractDouble( adapter, JsReplace("window.domAutomationController.send(" " (window.outerWidth - $1) / window.innerWidth);", border), &zoom_factor)); return zoom_factor; } double GetSubframeWidth(const ToRenderFrameHost& adapter) { double width; EXPECT_TRUE(ExecuteScriptAndExtractDouble( adapter, "window.domAutomationController.send(window.innerWidth);", &width)); return width; } // This struct is used to track changes to subframes after a main frame zoom // change, so that we can test subframe inner widths with assurance that all the // changes have finished propagating. struct FrameResizeObserver { FrameResizeObserver(RenderFrameHost* host, std::string label, double inner_width, double tolerance) : frame_host(host), msg_label(std::move(label)), zoomed_correctly(false), expected_inner_width(inner_width), tolerance(tolerance) { SetupOnResizeCallback(host, msg_label); } void SetupOnResizeCallback(const ToRenderFrameHost& adapter, const std::string& label) { const char kOnResizeCallbackSetup[] = "document.body.onresize = function(){" " window.domAutomationController.send('%s ' + window.innerWidth);" "};"; EXPECT_TRUE(ExecuteScript( adapter, base::StringPrintf(kOnResizeCallbackSetup, label.c_str()))); } void Check(const std::string& status_msg) { if (!base::StartsWith(status_msg, msg_label, base::CompareCase::SENSITIVE)) return; double inner_width = std::stod(status_msg.substr(msg_label.length() + 1)); zoomed_correctly = std::abs(expected_inner_width - inner_width) < tolerance; } FrameResizeObserver* toThis() {return this;} RenderFrameHost* frame_host; std::string msg_label; bool zoomed_correctly; double expected_inner_width; double tolerance; }; // This struct is used to wait until a resize has occurred. struct ResizeObserver { ResizeObserver(RenderFrameHost* host) : frame_host(host) { SetupOnResizeCallback(host); } void SetupOnResizeCallback(const ToRenderFrameHost& adapter) { const char kOnResizeCallbackSetup[] = "document.body.onresize = function(){" " window.domAutomationController.send('Resized');" "};"; EXPECT_TRUE(ExecuteScript( adapter, kOnResizeCallbackSetup)); } bool IsResizeCallback(const std::string& status_msg) { return status_msg == "Resized"; } RenderFrameHost* frame_host; }; void WaitForResize(DOMMessageQueue& msg_queue, ResizeObserver& observer) { std::string status; while (msg_queue.WaitForMessage(&status)) { // Strip the double quotes from the message. status = status.substr(1, status.length() -2); if (observer.IsResizeCallback(status)) break; } } void WaitAndCheckFrameZoom( DOMMessageQueue& msg_queue, std::vector& frame_observers) { std::string status; while (msg_queue.WaitForMessage(&status)) { // Strip the double quotes from the message. status = status.substr(1, status.length() -2); bool all_zoomed_correctly = true; // Use auto& to operate on a reference, and not a copy. for (auto& observer : frame_observers) { observer.Check(status); all_zoomed_correctly = all_zoomed_correctly && observer.zoomed_correctly; } if (all_zoomed_correctly) break; } } } // namespace IN_PROC_BROWSER_TEST_F(ZoomBrowserTest, ZoomPreservedOnReload) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b(a))")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_zoom_factor = 2.5; // Set the new zoom, wait for the page to be resized, and sanity-check that // the zoom was applied. { DOMMessageQueue msg_queue; ResizeObserver observer(root->current_frame_host()); const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level); WaitForResize(msg_queue, observer); } // Make this comparison approximate for Nexus5X test; // https://crbug.com/622858. EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01); // Now the actual test: Reload the page and check that the main frame is // still properly zoomed. WindowedNotificationObserver load_stop_observer( NOTIFICATION_LOAD_STOP, NotificationService::AllSources()); shell()->Reload(); load_stop_observer.Wait(); EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01); } IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframesZoomProperly) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b(a))")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); RenderFrameHostImpl* child = root->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host(); // The following calls must be made when the page's scale factor = 1.0. double scale_one_child_width = GetSubframeWidth(child); double scale_one_grandchild_width = GetSubframeWidth(grandchild); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_zoom_factor = 2.5; { DOMMessageQueue msg_queue; std::vector frame_observers; frame_observers.emplace_back(child, "child", scale_one_child_width, kTolerance); frame_observers.emplace_back(grandchild, "grandchild", scale_one_grandchild_width, kTolerance); const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level); WaitAndCheckFrameZoom(msg_queue, frame_observers); } // Make this comparison approximate for Nexus5X test; // https://crbug.com/622858. EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01); } IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframesDontZoomIndependently) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b(a))")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); RenderFrameHostImpl* child = root->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host(); // The following calls must be made when the page's scale factor = 1.0. double scale_one_child_width = GetSubframeWidth(child); double scale_one_grandchild_width = GetSubframeWidth(grandchild); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_zoom_factor = 2.0; const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); // This should not cause the nested iframe to change its zoom. host_zoom_map->SetZoomLevelForHost("b.com", new_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); EXPECT_EQ(scale_one_child_width, GetSubframeWidth(child)); EXPECT_EQ(scale_one_grandchild_width, GetSubframeWidth(grandchild)); // When we navigate so that b.com is the top-level site, then it has the // expected zoom. GURL new_url = embedded_test_server()->GetURL("b.com", "/title1.html"); EXPECT_TRUE(NavigateToURL(shell(), new_url)); EXPECT_DOUBLE_EQ( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); } IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, AllFramesGetDefaultZoom) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b(a))")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); RenderFrameHostImpl* child = root->child_at(0)->current_frame_host(); RenderFrameHostImpl* grandchild = root->child_at(0)->child_at(0)->current_frame_host(); // The following calls must be made when the page's scale factor = 1.0. double scale_one_child_width = GetSubframeWidth(child); double scale_one_grandchild_width = GetSubframeWidth(grandchild); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_default_zoom_factor = 2.0; { DOMMessageQueue msg_queue; std::vector frame_observers; frame_observers.emplace_back(child, "child", scale_one_child_width, kTolerance); frame_observers.emplace_back(grandchild, "grandchild", scale_one_grandchild_width, kTolerance); const double new_default_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_default_zoom_factor); host_zoom_map->SetZoomLevelForHost("b.com", new_default_zoom_level + 1.0); host_zoom_map->SetDefaultZoomLevel(new_default_zoom_level); WaitAndCheckFrameZoom(msg_queue, frame_observers); } // Make this comparison approximate for Nexus5X test; // https://crbug.com/622858. EXPECT_NEAR( new_default_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01 ); } // Flaky on mac, https://crbug.com/1055282 #if defined(OS_MAC) #define MAYBE_SiblingFramesZoom DISABLED_SiblingFramesZoom #else #define MAYBE_SiblingFramesZoom SiblingFramesZoom #endif IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, MAYBE_SiblingFramesZoom) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b,b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); RenderFrameHostImpl* child1 = root->child_at(0)->current_frame_host(); RenderFrameHostImpl* child2 = root->child_at(1)->current_frame_host(); // The following calls must be made when the page's scale factor = 1.0. double scale_one_child1_width = GetSubframeWidth(child1); double scale_one_child2_width = GetSubframeWidth(child2); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_zoom_factor = 2.5; { DOMMessageQueue msg_queue; std::vector frame_observers; frame_observers.emplace_back(child1, "child1", scale_one_child1_width, kTolerance); frame_observers.emplace_back(child2, "child2", scale_one_child2_width, kTolerance); const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level); WaitAndCheckFrameZoom(msg_queue, frame_observers); } // Make this comparison approximate for Nexus5X test; // https://crbug.com/622858. EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01); } IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframeRetainsZoomOnNavigation) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); FrameTreeNode* root = static_cast(web_contents())->GetFrameTree()->root(); RenderFrameHostImpl* child = root->child_at(0)->current_frame_host(); // The following calls must be made when the page's scale factor = 1.0. double scale_one_child_width = GetSubframeWidth(child); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); const double new_zoom_factor = 0.5; { DOMMessageQueue msg_queue; std::vector frame_observers; frame_observers.emplace_back(child, "child", scale_one_child_width, kTolerance); const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); host_zoom_map->SetZoomLevelForHost(top_level_host, new_zoom_level); WaitAndCheckFrameZoom(msg_queue, frame_observers); } // Make this comparison approximate for Nexus5X test; // https://crbug.com/622858. EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01 ); // Navigate child frame cross site, and make sure zoom is the same. TestNavigationObserver observer(web_contents()); GURL url = embedded_test_server()->GetURL("c.com", "/title1.html"); NavigateFrameToURL(root->child_at(0), url); EXPECT_TRUE(observer.last_navigation_succeeded()); EXPECT_EQ(url, observer.last_navigation_url()); // Check that the child frame maintained the same scale after navigating // cross-site. double new_child_width = GetSubframeWidth(root->child_at(0)->current_frame_host()); EXPECT_EQ(scale_one_child_width, new_child_width); } // http://crbug.com/609213 IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, RedirectToPageWithSubframeZoomsCorrectly) { std::string initial_host("a.com"); std::string redirected_host("b.com"); EXPECT_TRUE(NavigateToURL(shell(), GURL(embedded_test_server()->GetURL( initial_host, "/title2.html")))); double main_frame_window_border = GetMainframeWindowBorder(web_contents()); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); // Set a zoom level for b.com before we navigate to it. const double kZoomFactorForRedirectedHost = 1.5; HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); host_zoom_map->SetZoomLevelForHost( redirected_host, blink::PageZoomFactorToZoomLevel(kZoomFactorForRedirectedHost)); // Navigation to a.com doesn't change the zoom level, but when it redirects // to b.com, and then a subframe loads, the zoom should change. GURL redirect_url(embedded_test_server()->GetURL( redirected_host, "/cross_site_iframe_factory.html?b(b)")); GURL url(embedded_test_server()->GetURL( initial_host, "/client-redirect?" + redirect_url.spec())); NavigateToURLBlockUntilNavigationsComplete(shell(), url, 2); EXPECT_TRUE(IsLastCommittedEntryOfPageType(web_contents(), PAGE_TYPE_NORMAL)); EXPECT_EQ(redirect_url, web_contents()->GetLastCommittedURL()); EXPECT_NEAR(kZoomFactorForRedirectedHost, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), 0.01); } // Tests that on cross-site navigation from a page that has a subframe, the // appropriate zoom is applied to the new page. // crbug.com/673065 IN_PROC_BROWSER_TEST_F(IFrameZoomBrowserTest, SubframesDontBreakConnectionToRenderer) { std::string top_level_host("a.com"); GURL main_url(embedded_test_server()->GetURL( top_level_host, "/page_with_iframe_and_link.html")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); NavigationEntry* entry = web_contents()->GetController().GetLastCommittedEntry(); ASSERT_TRUE(entry); GURL loaded_url = HostZoomMap::GetURLFromEntry(entry); EXPECT_EQ(top_level_host, loaded_url.host()); // The following calls must be made when the page's scale factor = 1.0. double main_frame_window_border = GetMainframeWindowBorder(web_contents()); HostZoomMap* host_zoom_map = HostZoomMap::GetForWebContents(web_contents()); double default_zoom_level = host_zoom_map->GetDefaultZoomLevel(); EXPECT_EQ(0.0, default_zoom_level); EXPECT_DOUBLE_EQ( 1.0, GetMainFrameZoomFactor(web_contents(), main_frame_window_border)); // Set a zoom for a host that will be navigated to below. const double new_zoom_factor = 2.0; const double new_zoom_level = default_zoom_level + blink::PageZoomFactorToZoomLevel(new_zoom_factor); host_zoom_map->SetZoomLevelForHost("foo.com", new_zoom_level); // Navigate forward in the same RFH to a site with that host via a // renderer-initiated navigation. { const char kReplacePortNumber[] = "window.domAutomationController.send(setPortNumber(%d));"; uint16_t port_number = embedded_test_server()->port(); bool success = false; EXPECT_TRUE(ExecuteScriptAndExtractBool( shell(), base::StringPrintf(kReplacePortNumber, port_number), &success)); TestNavigationObserver observer(shell()->web_contents()); GURL url = embedded_test_server()->GetURL("foo.com", "/title2.html"); success = false; EXPECT_TRUE(ExecuteScriptAndExtractBool( shell(), "window.domAutomationController.send(clickCrossSiteLink());", &success)); EXPECT_TRUE(success); EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); EXPECT_EQ(url, observer.last_navigation_url()); EXPECT_TRUE(observer.last_navigation_succeeded()); } // Check that the requested zoom has been applied to the new site. // NOTE: Local observation on Linux has shown that this comparison has to be // approximate. As the common failure mode would be that the zoom is ~1 // instead of ~2, this approximation shouldn't be problematic. EXPECT_NEAR( new_zoom_factor, GetMainFrameZoomFactor(web_contents(), main_frame_window_border), .1); } } // namespace content