| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/site_per_process_browsertest.h" |
| |
| #include "base/json/json_reader.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "cc/base/math_util.h" |
| #include "content/browser/renderer_host/cross_process_frame_connector.h" |
| #include "content/browser/renderer_host/input/synthetic_touchscreen_pinch_gesture.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_view_child_frame.h" |
| #include "content/common/input/actions_parser.h" |
| #include "content/public/browser/render_process_host_priority_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/hit_test_region_observer.h" |
| #include "content/public/test/test_frame_navigation_observer.h" |
| #include "content/test/render_document_feature.h" |
| #include "content/test/render_widget_host_visibility_observer.h" |
| #include "mojo/public/cpp/test_support/test_utils.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/window_tree_host.h" |
| #endif |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "content/browser/renderer_host/input/synthetic_touchpad_pinch_gesture.h" |
| #include "ui/base/test/scoped_preferred_scroller_style_mac.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "ui/aura/test/test_screen.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| double GetFrameDeviceScaleFactor(const ToRenderFrameHost& adapter) { |
| return EvalJs(adapter, "window.devicePixelRatio;").ExtractDouble(); |
| } |
| |
| // Layout child frames in cross_site_iframe_factory.html so that they are the |
| // same width as the viewport, and 75% of the height of the window. This is for |
| // testing viewport intersection. Note this does not recurse into child frames |
| // and re-layout in the same way since children might be in a different origin. |
| void LayoutNonRecursiveForTestingViewportIntersection( |
| const ToRenderFrameHost& execution_target) { |
| static const char kRafScript[] = R"( |
| let width = window.innerWidth; |
| let height = window.innerHeight * 0.75; |
| for (let i = 0; i < window.frames.length; i++) { |
| let child = document.getElementById("child-" + i); |
| child.width = width; |
| child.height = height; |
| } |
| )"; |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(execution_target, kRafScript, "") |
| .error.empty()); |
| } |
| |
| // Check |intersects_viewport| on widget and process. |
| bool CheckIntersectsViewport(bool expected, FrameTreeNode* node) { |
| RenderProcessHostPriorityClient::Priority priority = |
| node->current_frame_host()->GetRenderWidgetHost()->GetPriority(); |
| return priority.intersects_viewport == expected && |
| node->current_frame_host()->GetProcess()->GetIntersectsViewport() == |
| expected; |
| } |
| |
| // Helper function to generate a click on the given RenderWidgetHost. The |
| // mouse event is forwarded directly to the RenderWidgetHost without any |
| // hit-testing. |
| void SimulateMouseClick(RenderWidgetHost* rwh, int x, int y) { |
| blink::WebMouseEvent mouse_event( |
| blink::WebInputEvent::Type::kMouseDown, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| mouse_event.button = blink::WebPointerProperties::Button::kLeft; |
| mouse_event.SetPositionInWidget(x, y); |
| rwh->ForwardMouseEvent(mouse_event); |
| } |
| |
| } // namespace |
| |
| // Class to monitor incoming UpdateViewportIntersection messages. The caller has |
| // to guarantee that `rfph` lives at least as long as |
| // UpdateViewportIntersectionMessageFilter. |
| class UpdateViewportIntersectionMessageFilter |
| : public blink::mojom::RemoteFrameHostInterceptorForTesting { |
| public: |
| explicit UpdateViewportIntersectionMessageFilter( |
| content::RenderFrameProxyHost* rfph) |
| : intersection_state_(blink::mojom::ViewportIntersectionState::New()), |
| render_frame_proxy_host_(rfph), |
| swapped_impl_( |
| render_frame_proxy_host_->frame_host_receiver_for_testing(), |
| this) {} |
| |
| ~UpdateViewportIntersectionMessageFilter() override = default; |
| |
| const blink::mojom::ViewportIntersectionStatePtr& GetIntersectionState() |
| const { |
| return intersection_state_; |
| } |
| |
| RenderFrameProxyHost* GetForwardingInterface() override { |
| return render_frame_proxy_host_; |
| } |
| |
| void UpdateViewportIntersection( |
| blink::mojom::ViewportIntersectionStatePtr intersection_state, |
| const absl::optional<blink::FrameVisualProperties>& visual_properties) |
| override { |
| intersection_state_ = std::move(intersection_state); |
| msg_received_ = true; |
| if (run_loop_) |
| run_loop_->Quit(); |
| } |
| |
| bool MessageReceived() const { return msg_received_; } |
| |
| void Clear() { |
| msg_received_ = false; |
| intersection_state_ = blink::mojom::ViewportIntersectionState::New(); |
| } |
| |
| void Wait() { |
| DCHECK(!run_loop_); |
| if (msg_received_) { |
| msg_received_ = false; |
| return; |
| } |
| std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop); |
| run_loop_ = run_loop.get(); |
| run_loop_->Run(); |
| run_loop_ = nullptr; |
| msg_received_ = false; |
| } |
| |
| void set_run_loop(base::RunLoop* run_loop) { run_loop_ = run_loop; } |
| |
| private: |
| raw_ptr<base::RunLoop> run_loop_ = nullptr; |
| bool msg_received_; |
| blink::mojom::ViewportIntersectionStatePtr intersection_state_; |
| raw_ptr<content::RenderFrameProxyHost> render_frame_proxy_host_; |
| mojo::test::ScopedSwapImplForTesting< |
| mojo::AssociatedReceiver<blink::mojom::RemoteFrameHost>> |
| swapped_impl_; |
| }; |
| |
| // TODO(tonikitoo): Move to fake_remote_frame.h|cc in case it is useful |
| // for other tests. |
| class FakeRemoteMainFrame : public blink::mojom::RemoteMainFrame { |
| public: |
| FakeRemoteMainFrame() = default; |
| ~FakeRemoteMainFrame() override = default; |
| |
| void Init( |
| mojo::PendingAssociatedReceiver<blink::mojom::RemoteMainFrame> receiver) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| // blink::mojom::RemoteMainFrame overrides: |
| void UpdateTextAutosizerPageInfo( |
| blink::mojom::TextAutosizerPageInfoPtr page_info) override {} |
| |
| private: |
| mojo::AssociatedReceiver<blink::mojom::RemoteMainFrame> receiver_{this}; |
| }; |
| |
| // This class intercepts RenderFrameProxyHost creations, and overrides their |
| // respective blink::mojom::RemoteMainFrame instances, so that it can watch for |
| // text autosizer page info updates. |
| class UpdateTextAutosizerInfoProxyObserver |
| : public RenderFrameProxyHost::TestObserver { |
| public: |
| UpdateTextAutosizerInfoProxyObserver() { |
| RenderFrameProxyHost::SetObserverForTesting(this); |
| } |
| ~UpdateTextAutosizerInfoProxyObserver() override { |
| RenderFrameProxyHost::SetObserverForTesting(nullptr); |
| } |
| |
| const blink::mojom::TextAutosizerPageInfo& TextAutosizerPageInfo( |
| RenderFrameProxyHost* proxy) { |
| return remote_frames_[proxy]->page_info(); |
| } |
| |
| private: |
| class Remote : public FakeRemoteMainFrame { |
| public: |
| explicit Remote(RenderFrameProxyHost* proxy) { |
| Init(proxy->BindRemoteMainFrameReceiverForTesting()); |
| } |
| void UpdateTextAutosizerPageInfo( |
| blink::mojom::TextAutosizerPageInfoPtr page_info) override { |
| page_info_ = *page_info; |
| } |
| const blink::mojom::TextAutosizerPageInfo& page_info() { |
| return page_info_; |
| } |
| |
| private: |
| blink::mojom::TextAutosizerPageInfo page_info_; |
| }; |
| |
| void OnRemoteMainFrameBound(RenderFrameProxyHost* proxy_host) override { |
| remote_frames_[proxy_host] = std::make_unique<Remote>(proxy_host); |
| } |
| |
| std::map<RenderFrameProxyHost*, std::unique_ptr<Remote>> remote_frames_; |
| }; |
| |
| // Class to intercept incoming TextAutosizerPageInfoChanged messages. The caller |
| // has to guarantee that `render_frame_host` lives at least as long as |
| // TextAutosizerPageInfoInterceptor. |
| class TextAutosizerPageInfoInterceptor |
| : public blink::mojom::LocalMainFrameHostInterceptorForTesting { |
| public: |
| explicit TextAutosizerPageInfoInterceptor( |
| RenderFrameHostImpl* render_frame_host) |
| : render_frame_host_(render_frame_host), |
| swapped_impl_( |
| render_frame_host_->local_main_frame_host_receiver_for_testing(), |
| this) {} |
| |
| ~TextAutosizerPageInfoInterceptor() override = default; |
| |
| LocalMainFrameHost* GetForwardingInterface() override { |
| return render_frame_host_; |
| } |
| |
| void WaitForPageInfo(absl::optional<int> target_main_frame_width, |
| absl::optional<float> target_device_scale_adjustment) { |
| if (remote_page_info_seen_) |
| return; |
| target_main_frame_width_ = target_main_frame_width; |
| target_device_scale_adjustment_ = target_device_scale_adjustment; |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| run_loop_->Run(); |
| run_loop_.reset(); |
| } |
| |
| const blink::mojom::TextAutosizerPageInfo& GetTextAutosizerPageInfo() { |
| return *remote_page_info_; |
| } |
| |
| void TextAutosizerPageInfoChanged( |
| blink::mojom::TextAutosizerPageInfoPtr remote_page_info) override { |
| if ((!target_main_frame_width_ || |
| remote_page_info->main_frame_width != target_main_frame_width_) && |
| (!target_device_scale_adjustment_ || |
| remote_page_info->device_scale_adjustment != |
| target_device_scale_adjustment_)) { |
| return; |
| } |
| remote_page_info_ = remote_page_info.Clone(); |
| remote_page_info_seen_ = true; |
| if (run_loop_) |
| run_loop_->Quit(); |
| GetForwardingInterface()->TextAutosizerPageInfoChanged( |
| std::move(remote_page_info)); |
| } |
| |
| private: |
| raw_ptr<RenderFrameHostImpl> render_frame_host_; |
| bool remote_page_info_seen_ = false; |
| blink::mojom::TextAutosizerPageInfoPtr remote_page_info_ = |
| blink::mojom::TextAutosizerPageInfo::New(/*main_frame_width=*/0, |
| /*main_frame_layout_width=*/0, |
| /*device_scale_adjustment=*/1.f); |
| std::unique_ptr<base::RunLoop> run_loop_; |
| absl::optional<int> target_main_frame_width_; |
| absl::optional<float> target_device_scale_adjustment_; |
| mojo::test::ScopedSwapImplForTesting< |
| mojo::AssociatedReceiver<blink::mojom::LocalMainFrameHost>> |
| swapped_impl_; |
| }; |
| |
| class SitePerProcessHighDPIBrowserTest : public SitePerProcessBrowserTest { |
| public: |
| const double kDeviceScaleFactor = 2.0; |
| |
| SitePerProcessHighDPIBrowserTest() = default; |
| |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| SitePerProcessBrowserTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII( |
| switches::kForceDeviceScaleFactor, |
| base::StringPrintf("%f", kDeviceScaleFactor)); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessHighDPIBrowserTest, |
| SubframeLoadsWithCorrectDeviceScaleFactor) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // On Android forcing device scale factor does not work for tests, therefore |
| // we ensure that make frame and iframe have the same DIP scale there, but |
| // not necessarily kDeviceScaleFactor. |
| const double expected_dip_scale = |
| #if BUILDFLAG(IS_ANDROID) |
| GetFrameDeviceScaleFactor(web_contents()); |
| #else |
| SitePerProcessHighDPIBrowserTest::kDeviceScaleFactor; |
| #endif |
| |
| EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(web_contents())); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(root)); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* child = root->child_at(0); |
| EXPECT_EQ(expected_dip_scale, GetFrameDeviceScaleFactor(child)); |
| } |
| |
| class SitePerProcessCompositorViewportBrowserTest |
| : public SitePerProcessBrowserTestBase, |
| public testing::WithParamInterface<double> { |
| public: |
| SitePerProcessCompositorViewportBrowserTest() = default; |
| |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| SitePerProcessBrowserTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII(switches::kForceDeviceScaleFactor, |
| base::StringPrintf("%f", GetParam())); |
| } |
| }; |
| |
| // DISABLED: crbug.com/1071995 |
| IN_PROC_BROWSER_TEST_P(SitePerProcessCompositorViewportBrowserTest, |
| DISABLED_OopifCompositorViewportSizeRelativeToParent) { |
| // Load page with very tall OOPIF. |
| GURL main_url( |
| embedded_test_server()->GetURL("a.com", "/super_tall_parent.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* child = root->child_at(0); |
| |
| GURL nested_site_url( |
| embedded_test_server()->GetURL("b.com", "/super_tall_page.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(child, nested_site_url)); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| // Observe frame submission from parent. |
| RenderFrameSubmissionObserver parent_observer( |
| root->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->render_frame_metadata_provider()); |
| parent_observer.WaitForAnyFrameSubmission(); |
| gfx::Size parent_viewport_size = |
| parent_observer.LastRenderFrameMetadata().viewport_size_in_pixels; |
| |
| // Observe frame submission from child. |
| RenderFrameSubmissionObserver child_observer( |
| child->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->render_frame_metadata_provider()); |
| child_observer.WaitForAnyFrameSubmission(); |
| gfx::Size child_viewport_size = |
| child_observer.LastRenderFrameMetadata().viewport_size_in_pixels; |
| |
| // Verify child's compositor viewport is no more than about 30% larger than |
| // the parent's. See RemoteFrameView::GetCompositingRect() for explanation of |
| // the choice of 30%. Add +1 to child viewport height to account for rounding. |
| EXPECT_GE(ceilf(1.3f * parent_viewport_size.height()), |
| child_viewport_size.height() - 1); |
| |
| // Verify the child's ViewBounds are much larger. |
| RenderWidgetHostViewBase* child_rwhv = static_cast<RenderWidgetHostViewBase*>( |
| child->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| // 30,000 is based on div/iframe sizes in the test HTML files. |
| EXPECT_LT(30000, child_rwhv->GetViewBounds().height()); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android doesn't support forcing device scale factor in tests. |
| INSTANTIATE_TEST_SUITE_P(SitePerProcess, |
| SitePerProcessCompositorViewportBrowserTest, |
| testing::Values(1.0)); |
| #else |
| INSTANTIATE_TEST_SUITE_P(SitePerProcess, |
| SitePerProcessCompositorViewportBrowserTest, |
| testing::Values(1.0, 1.5, 2.0)); |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| SubframeUpdateToCorrectDeviceScaleFactor) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| EXPECT_EQ(1.0, GetFrameDeviceScaleFactor(web_contents())); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* child = root->child_at(0); |
| EXPECT_EQ(1.0, GetFrameDeviceScaleFactor(child)); |
| |
| double expected_dip_scale = 2.0; |
| |
| // TODO(oshima): allow DeviceScaleFactor change on other platforms |
| // (win, linux, mac, android and mus). |
| aura::TestScreen* test_screen = |
| static_cast<aura::TestScreen*>(display::Screen::GetScreen()); |
| test_screen->CreateHostForPrimaryDisplay(); |
| test_screen->SetDeviceScaleFactor(expected_dip_scale); |
| |
| // This forces |expected_dip_scale| to be applied to the aura::WindowTreeHost |
| // and aura::Window. |
| aura::WindowTreeHost* window_tree_host = shell()->window()->GetHost(); |
| window_tree_host->SetBoundsInPixels(window_tree_host->GetBoundsInPixels()); |
| |
| // Wait until dppx becomes 2 if the frame's dpr hasn't beeen updated |
| // to 2 yet. |
| const char kScript[] = R"( |
| new Promise(resolve => { |
| if (window.devicePixelRatio == 2) |
| resolve(window.devicePixelRatio); |
| window.matchMedia('screen and (min-resolution: 2dppx)') |
| .addListener(function(e) { |
| if (e.matches) { |
| resolve(window.devicePixelRatio); |
| } |
| }); |
| }); |
| )"; |
| // Make sure that both main frame and iframe are updated to 2x. |
| EXPECT_EQ(expected_dip_scale, EvalJs(child, kScript).ExtractDouble()); |
| |
| EXPECT_EQ(expected_dip_scale, |
| EvalJs(web_contents(), kScript).ExtractDouble()); |
| } |
| |
| #endif |
| |
| // Tests that when a large OOPIF has been scaled, the compositor raster area |
| // sent from the embedder is correct. |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) |
| // Temporarily disabled on Android because this doesn't account for browser |
| // control height or page scale factor. |
| // Flaky on Mac. https://crbug.com/840314 |
| #define MAYBE_ScaledIframeRasterSize DISABLED_ScaledframeRasterSize |
| #else |
| #define MAYBE_ScaledIframeRasterSize ScaledIframeRasterSize |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| MAYBE_ScaledIframeRasterSize) { |
| GURL http_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_scaled_large_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| FrameTreeNode* child = root->child_at(0); |
| RenderFrameProxyHost* child_proxy = |
| child->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy); |
| |
| // Force a lifecycle update and wait for it to finish; by the time this call |
| // returns, the viewport intersection IPC should already have been received |
| // by the browser process and handled by the filter. |
| EvalJsResult eval_result = EvalJsAfterLifecycleUpdate( |
| root->current_frame_host(), |
| "document.getElementsByTagName('div')[0].scrollTo(0, 5000);", |
| "document.getElementsByTagName('div')[0].getBoundingClientRect().top;"); |
| ASSERT_TRUE(eval_result.error.empty()); |
| int div_offset_top = eval_result.ExtractInt(); |
| gfx::Rect compositing_rect = |
| filter->GetIntersectionState()->compositor_visible_rect; |
| |
| float device_scale_factor = 1.0f; |
| device_scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents()); |
| |
| // The math below replicates the calculations in |
| // RemoteFrameView::GetCompositingRect(). That could be subject to tweaking, |
| // which would have to be reflected in these test expectations. Also, any |
| // changes to Blink that would affect the size of the frame rect or the |
| // visible viewport would need to be accounted for. |
| // The multiplication by 5 accounts for the 0.2 scale factor in the test, |
| // which increases the area that has to be drawn in the OOPIF. |
| int view_height = root->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView() |
| ->GetViewBounds() |
| .height() * |
| 5 * device_scale_factor; |
| |
| // The raster size is expanded by a factor of 1.3 to allow for some scrolling |
| // without requiring re-raster. The expanded area to be rasterized should be |
| // centered around the iframe's visible area within the parent document, hence |
| // the expansion in each direction (top, bottom, left, right) is |
| // (0.15 * viewport dimension). |
| int expansion = ceilf(view_height * 0.15f); |
| int expected_height = view_height + expansion * 2; |
| |
| // 5000 = div scroll offset in scaled pixels |
| // 5 = scale factor from top-level document to iframe contents |
| // 2 = iframe border in scaled pixels |
| int expected_offset = |
| ((5000 - (div_offset_top * 5) - 2) * device_scale_factor) - expansion; |
| |
| // Allow a small amount for rounding differences from applying page and |
| // device scale factors at different times. |
| float tolerance = ceilf(device_scale_factor); |
| EXPECT_NEAR(compositing_rect.height(), expected_height, tolerance); |
| EXPECT_NEAR(compositing_rect.y(), expected_offset, tolerance); |
| } |
| |
| // Similar to ScaledIFrameRasterSize but with nested OOPIFs to ensure |
| // propagation works correctly. |
| #if BUILDFLAG(IS_ANDROID) |
| // Temporarily disabled on Android because this doesn't account for browser |
| // control height or page scale factor. |
| #define MAYBE_ScaledNestedIframeRasterSize DISABLED_ScaledNestedIframeRasterSize |
| #else |
| #define MAYBE_ScaledNestedIframeRasterSize ScaledNestedIframeRasterSize |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| MAYBE_ScaledNestedIframeRasterSize) { |
| GURL http_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_scaled_large_frames_nested.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| FrameTreeNode* child_b = root->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_b, |
| embedded_test_server()->GetURL( |
| "bar.com", "/frame_tree/page_with_large_scrollable_frame.html"))); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C\n" |
| " +--Site B ------- proxies for A C\n" |
| " +--Site C -- proxies for A B\n" |
| "Where A = http://a.com/\n" |
| " B = http://bar.com/\n" |
| " C = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| // This adds the filter to the immediate child iframe. It verifies that the |
| // child sets the nested iframe's compositing rect correctly. |
| FrameTreeNode* child_c = child_b->child_at(0); |
| RenderFrameProxyHost* child_c_proxy = |
| child_c->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_c_proxy); |
| |
| // Scroll the child frame so that it is partially clipped. This will cause the |
| // top 10 pixels of the child frame to be clipped. Applying the scale factor |
| // means that in the coordinate system of the subframes, 50px are clipped. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), |
| "window.scrollBy(0, 10)", "") |
| .error.empty()); |
| |
| // This scrolls the div containing in the 'Site B' iframe that contains the |
| // 'Site C' iframe, and then we verify that the 'Site C' frame receives the |
| // correct compositor frame. Force a lifecycle update after the scroll and |
| // wait for it to finish; by the time this call returns, the viewport |
| // intersection IPC should already have been received by the browser process |
| // and handled by the filter. Extract the page offset of the leaf iframe |
| // within the middle document. |
| EvalJsResult child_eval_result = EvalJsAfterLifecycleUpdate( |
| child_b->current_frame_host(), |
| "document.getElementsByTagName('div')[0].scrollTo(0, 5000);", |
| "document.getElementsByTagName('div')[0].getBoundingClientRect().top;"); |
| ASSERT_TRUE(child_eval_result.error.empty()); |
| int child_div_offset_top = child_eval_result.ExtractInt(); |
| |
| gfx::Rect compositing_rect = |
| filter->GetIntersectionState()->compositor_visible_rect; |
| |
| float scale_factor = 1.0f; |
| scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents()); |
| |
| // See comment in ScaledIframeRasterSize for explanation of this. In this |
| // case, the raster area of the large iframe should be restricted to |
| // approximately the area of its containing frame which is unclipped by the |
| // main frame. The containing frame is clipped by 50 pixels at the top, due |
| // to the scroll offset of the main frame, so we subtract that from the full |
| // height of the containing frame. |
| int view_height = (child_b->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView() |
| ->GetViewBounds() |
| .height() - |
| 50) * |
| scale_factor; |
| // 30% padding is added to the view_height to prevent frequent re-rasters. |
| // The extra padding is centered around the view height, hence expansion by |
| // 0.15 in each direction. |
| int expansion = ceilf(view_height * 0.15f); |
| int expected_height = view_height + expansion * 2; |
| |
| // Explanation of terms: |
| // 5000 = offset from top of nested iframe to top of containing div, due to |
| // scroll offset of div |
| // child_div_offset_top = offset of containing div from top of child frame |
| // 50 = offset of child frame's intersection with the top document viewport |
| // from the top of the child frame (i.e, clipped amount at top of child) |
| // view_height * 0.15 = padding added to the top of the compositing rect |
| // (half the the 30% total padding) |
| int expected_offset = |
| 5000 - ((child_div_offset_top - 50) * scale_factor) - expansion; |
| |
| // Allow a small amount for rounding differences from applying page and |
| // device scale factors at different times. |
| EXPECT_NEAR(compositing_rect.height(), expected_height, ceilf(scale_factor)); |
| EXPECT_NEAR(compositing_rect.y(), expected_offset, ceilf(scale_factor)); |
| } |
| |
| // Tests that when an OOPIF is inside a multicolumn container, its compositing |
| // rect is set correctly. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| IframeInMulticolCompositingRect) { |
| GURL http_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_iframe_in_multicol.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| FrameTreeNode* child = root->child_at(0); |
| RenderFrameProxyHost* child_proxy = |
| child->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy); |
| |
| // Force a lifecycle update and wait for it to finish. Changing the width of |
| // the iframe should cause the parent renderer to propagate a new |
| // ViewportIntersectionState while running the rendering pipeline. By the time |
| // this call returns, the viewport intersection IPC should already have been |
| // received by the browser process and handled by the filter. |
| EvalJsResult eval_result = EvalJsAfterLifecycleUpdate( |
| root->current_frame_host(), |
| "document.querySelector('iframe').style.width = '250px'", ""); |
| ASSERT_TRUE(filter->MessageReceived()); |
| gfx::Rect compositing_rect = |
| filter->GetIntersectionState()->compositor_visible_rect; |
| |
| float scale_factor = 1.0f; |
| scale_factor = GetFrameDeviceScaleFactor(shell()->web_contents()); |
| |
| gfx::Point visible_offset(0, 0); |
| gfx::Size visible_size = |
| gfx::ScaleToFlooredSize(gfx::Size(250, 150), scale_factor, scale_factor); |
| gfx::Rect visible_rect(visible_offset, visible_size); |
| float tolerance = ceilf(scale_factor); |
| EXPECT_NEAR(compositing_rect.x(), visible_rect.x(), tolerance); |
| EXPECT_NEAR(compositing_rect.y(), visible_rect.y(), tolerance); |
| EXPECT_NEAR(compositing_rect.width(), visible_rect.width(), tolerance); |
| EXPECT_NEAR(compositing_rect.height(), visible_rect.height(), tolerance); |
| EXPECT_TRUE(compositing_rect.Contains(visible_rect)); |
| } |
| |
| // Flaky on multiple platforms (crbug.com/1094562). |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_FrameViewportIntersectionTestSimple) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(c),d,e(f))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| RenderFrameProxyHost* child2_proxy = |
| root->child_at(2)->render_manager()->GetProxyToParent(); |
| auto child2_filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child2_proxy); |
| |
| // Force lifecycle update in root and child2 to make sure child2 has sent |
| // viewport intersection into to grand child before child2 becomes throttled. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "") |
| .error.empty()); |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate( |
| root->child_at(2)->current_frame_host(), "", "") |
| .error.empty()); |
| child2_filter->Clear(); |
| |
| LayoutNonRecursiveForTestingViewportIntersection(shell()->web_contents()); |
| |
| // Root should always intersect. |
| EXPECT_TRUE(CheckIntersectsViewport(true, root)); |
| // Child 0 should be entirely in viewport. |
| EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(0))); |
| // Make sure child0 has has a chance to propagate viewport intersection to |
| // grand child. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate( |
| root->child_at(0)->current_frame_host(), "", "") |
| .error.empty()); |
| // Grand child should match parent. |
| EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(0)->child_at(0))); |
| // Child 1 should be partially in viewport. |
| EXPECT_TRUE(CheckIntersectsViewport(true, root->child_at(1))); |
| // Child 2 should be not be in viewport. |
| EXPECT_TRUE(CheckIntersectsViewport(false, root->child_at(2))); |
| // Can't use EvalJsAfterLifecycleUpdate on child2, because it's |
| // render-throttled. But it should still have propagated state down to the |
| // grandchild. |
| child2_filter->Wait(); |
| // Grand child should match parent. |
| EXPECT_TRUE(CheckIntersectsViewport(false, root->child_at(2)->child_at(0))); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| FrameViewportOffsetTestSimple) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(c))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // This will catch b sending viewport intersection information to c. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| RenderFrameProxyHost* iframe_c_proxy = |
| root->child_at(0)->child_at(0)->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(iframe_c_proxy); |
| |
| // Use EvalJsAfterLifecycleUpdate to force animation frames in `a` and `b` to |
| // ensure that the viewport intersection for initial layout state has been |
| // propagated. The layout of `a` will not change again, so we can read back |
| // its layout info after the animation frame. The layout of `b` will change, |
| // so we don't read back its layout yet. |
| std::string script(R"( |
| let iframe = document.querySelector("iframe"); |
| [iframe.offsetLeft, iframe.offsetTop]; |
| )"); |
| EvalJsResult iframe_b_result = |
| EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", script); |
| base::Value iframe_b_offset = iframe_b_result.ExtractList(); |
| int iframe_b_offset_left = iframe_b_offset.GetList()[0].GetInt(); |
| int iframe_b_offset_top = iframe_b_offset.GetList()[1].GetInt(); |
| |
| // Make sure a new IPC is sent after dirty-ing layout. |
| filter->Clear(); |
| |
| // Dirty layout in `b` to generate a new IPC to `c`. This will be the final |
| // layout state for `b`, so read back layout info here. |
| std::string raf_script(R"( |
| let iframe = document.querySelector("iframe"); |
| let margin = getComputedStyle(iframe).marginTop.replace("px", ""); |
| iframe.style.margin = String(parseInt(margin) + 1) + "px"; |
| )"); |
| EvalJsResult iframe_c_result = EvalJsAfterLifecycleUpdate( |
| root->child_at(0)->current_frame_host(), raf_script, script); |
| base::Value iframe_c_offset = iframe_c_result.ExtractList(); |
| int iframe_c_offset_left = iframe_c_offset.GetList()[0].GetInt(); |
| int iframe_c_offset_top = iframe_c_offset.GetList()[1].GetInt(); |
| |
| // The IPC should already have been sent |
| EXPECT_TRUE(filter->MessageReceived()); |
| |
| // +4 for a 2px border on each iframe. |
| gfx::Vector2dF expected(iframe_b_offset_left + iframe_c_offset_left + 4, |
| iframe_b_offset_top + iframe_c_offset_top + 4); |
| const float device_scale_factor = |
| root->render_manager()->GetRenderWidgetHostView()->GetDeviceScaleFactor(); |
| // Convert from CSS to physical pixels |
| expected.Scale(device_scale_factor); |
| gfx::Transform actual = filter->GetIntersectionState()->main_frame_transform; |
| const absl::optional<gfx::PointF> viewport_offset_source_point = |
| actual.InverseMapPoint(gfx::PointF()); |
| ASSERT_TRUE(viewport_offset_source_point.has_value()); |
| const gfx::Vector2dF viewport_offset = |
| gfx::PointF() - viewport_offset_source_point.value(); |
| float tolerance = ceilf(device_scale_factor); |
| EXPECT_NEAR(expected.x(), viewport_offset.x(), tolerance); |
| EXPECT_NEAR(expected.y(), viewport_offset.y(), tolerance); |
| } |
| |
| // TODO(crbug.com/1168036): Flaky test. |
| IN_PROC_BROWSER_TEST_P( |
| SitePerProcessBrowserTest, |
| DISABLED_NestedIframeTransformedIntoViewViewportIntersection) { |
| GURL http_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_frame_transformed_into_viewport.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| FrameTreeNode* child_b = root->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_b, |
| embedded_test_server()->GetURL( |
| "bar.com", "/frame_tree/page_with_cross_origin_frame_at_half.html"))); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C\n" |
| " +--Site B ------- proxies for A C\n" |
| " +--Site C -- proxies for A B\n" |
| "Where A = http://a.com/\n" |
| " B = http://bar.com/\n" |
| " C = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* child_c = child_b->child_at(0); |
| RenderFrameProxyHost* child_c_proxy = |
| child_c->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_c_proxy); |
| |
| // Scroll the div containing the 'Site B' iframe to trigger a viewport |
| // intersection update. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate( |
| child_b->current_frame_host(), |
| "document.getElementsByTagName('div')[0].scrollTo(0, 5000);", |
| "") |
| .error.empty()); |
| ASSERT_TRUE(filter->MessageReceived()); |
| |
| // Check that we currently intersect with the viewport. |
| gfx::Rect viewport_intersection = |
| filter->GetIntersectionState()->viewport_intersection; |
| |
| EXPECT_GT(viewport_intersection.height(), 0); |
| EXPECT_GT(viewport_intersection.width(), 0); |
| } |
| |
| // Verify that OOPIF select element popup menu coordinates account for scroll |
| // offset in containers embedding frame. |
| // TODO(crbug.com/859552): Reenable this. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_PopupMenuInTallIframeTest) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "/frame_tree/page_with_tall_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child_node = root->child_at(0); |
| GURL site_url(embedded_test_server()->GetURL( |
| "baz.com", "/site_isolation/page-with-select.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(child_node, site_url)); |
| |
| RenderFrameProxyHost* root_proxy = root->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(root_proxy); |
| |
| // Position the select element so that it is out of the viewport, then scroll |
| // it into view. |
| EXPECT_TRUE(ExecJs(child_node, |
| "document.querySelector('select').style.top='2000px';")); |
| EXPECT_TRUE(ExecJs(root, "window.scrollTo(0, 1900);")); |
| |
| // Wait for a viewport intersection update to be dispatched to the child, and |
| // ensure it is processed by the browser before continuing. |
| filter->Wait(); |
| { |
| // This yields the UI thread in order to ensure that the new viewport |
| // intersection is sent to the to child renderer before the mouse click |
| // below. |
| base::RunLoop loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, loop.QuitClosure()); |
| loop.Run(); |
| } |
| |
| auto show_popup_waiter = std::make_unique<ShowPopupWidgetWaiter>( |
| web_contents(), child_node->current_frame_host()); |
| SimulateMouseClick(child_node->current_frame_host()->GetRenderWidgetHost(), |
| 55, 2005); |
| |
| // Dismiss the popup. |
| SimulateMouseClick(child_node->current_frame_host()->GetRenderWidgetHost(), 1, |
| 1); |
| |
| // The test passes if this wait returns, indicating that the popup was |
| // scrolled into view and the OOPIF renderer displayed it. Other tests verify |
| // the correctness of popup menu coordinates. |
| show_popup_waiter->Wait(); |
| } |
| |
| // Test to verify that viewport intersection is propagated to nested OOPIFs |
| // even when a parent OOPIF has been throttled. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| NestedFrameViewportIntersectionUpdated) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "foo.com", "/frame_tree/scrollable_page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child_node = root->child_at(0); |
| GURL site_url(embedded_test_server()->GetURL( |
| "bar.com", "/frame_tree/page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(child_node, site_url)); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C\n" |
| " +--Site B ------- proxies for A C\n" |
| " +--Site C -- proxies for A B\n" |
| "Where A = http://foo.com/\n" |
| " B = http://bar.com/\n" |
| " C = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| // This will intercept messages sent from B to C, describing C's viewport |
| // intersection. |
| RenderFrameProxyHost* child_proxy = |
| child_node->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy); |
| |
| // Run requestAnimationFrame in A and B to make sure initial layout has |
| // completed and initial IPCs sent. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "") |
| .error.empty()); |
| ASSERT_TRUE( |
| EvalJsAfterLifecycleUpdate(child_node->current_frame_host(), "", "") |
| .error.empty()); |
| filter->Clear(); |
| |
| // Scroll the child frame out of view, causing it to become throttled. |
| ASSERT_TRUE(ExecJs(root->current_frame_host(), "window.scrollTo(0, 5000)")); |
| filter->Wait(); |
| EXPECT_TRUE(filter->GetIntersectionState()->viewport_intersection.IsEmpty()); |
| |
| // Scroll the frame back into view. |
| ASSERT_TRUE(ExecJs(root->current_frame_host(), "window.scrollTo(0, 0)")); |
| filter->Wait(); |
| EXPECT_FALSE(filter->GetIntersectionState()->viewport_intersection.IsEmpty()); |
| } |
| |
| // Test to verify that the main frame document intersection |
| // is propagated to out of process iframes by scrolling a nested iframe |
| // in and out of intersecting with the main frame document. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| NestedFrameMainFrameDocumentIntersectionUpdated) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "foo.com", "/frame_tree/scrollable_page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child_node_b = root->child_at(0); |
| GURL site_url(embedded_test_server()->GetURL( |
| "bar.com", "/frame_tree/scrollable_page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(child_node_b, site_url)); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C\n" |
| " +--Site B ------- proxies for A C\n" |
| " +--Site C -- proxies for A B\n" |
| "Where A = http://foo.com/\n" |
| " B = http://bar.com/\n" |
| " C = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* child_node_c = child_node_b->child_at(0); |
| RenderFrameProxyHost* child_proxy_c = |
| child_node_c->render_manager()->GetProxyToParent(); |
| auto filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(child_proxy_c); |
| |
| // Run requestAnimationFrame in A and B to make sure initial layout has |
| // completed and initial IPC's sent. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(root->current_frame_host(), "", "") |
| .error.empty()); |
| ASSERT_TRUE( |
| EvalJsAfterLifecycleUpdate(child_node_b->current_frame_host(), "", "") |
| .error.empty()); |
| filter->Clear(); |
| |
| // Scroll the child frame out of view, causing it to become throttled. |
| ASSERT_TRUE( |
| ExecJs(child_node_b->current_frame_host(), "window.scrollTo(0, 5000)")); |
| filter->Wait(); |
| EXPECT_TRUE( |
| filter->GetIntersectionState()->main_frame_intersection.IsEmpty()); |
| |
| // Scroll the frame back into view. |
| ASSERT_TRUE( |
| ExecJs(child_node_b->current_frame_host(), "window.scrollTo(0, 0)")); |
| filter->Wait(); |
| EXPECT_FALSE( |
| filter->GetIntersectionState()->main_frame_intersection.IsEmpty()); |
| } |
| |
| // Tests that outermost_main_frame_scroll_position is not shared by frames in |
| // the same process. This is a regression test for https://crbug.com/1063760. |
| // |
| // Set up the frame tree to be A(B1(C1),B2(C2)). Send IPC's with different |
| // ViewportIntersection information to B1 and B2, and then check that the |
| // information they propagate to C1 and C2 is different. |
| // Disabled because of https://crbug.com/1136263 |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_MainFrameScrollOffset) { |
| GURL a_url = embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/scrollable_page_with_two_frames.html"); |
| GURL b_url = embedded_test_server()->GetURL( |
| "b.com", "/frame_tree/page_with_large_iframe.html"); |
| GURL c_url = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), a_url)); |
| FrameTreeNode* a_node = web_contents()->GetPrimaryFrameTree().root(); |
| |
| FrameTreeNode* b1_node = a_node->child_at(0); |
| EXPECT_TRUE(NavigateToURLFromRenderer(b1_node, b_url)); |
| |
| FrameTreeNode* c1_node = b1_node->child_at(0); |
| EXPECT_TRUE(NavigateToURLFromRenderer(c1_node, c_url)); |
| |
| FrameTreeNode* b2_node = a_node->child_at(1); |
| EXPECT_TRUE(NavigateToURLFromRenderer(b2_node, b_url)); |
| |
| FrameTreeNode* c2_node = b2_node->child_at(0); |
| EXPECT_TRUE(NavigateToURLFromRenderer(c2_node, c_url)); |
| |
| // This will intercept messages sent from B1 to C1, describing C1's viewport |
| // intersection. |
| RenderFrameProxyHost* c1_proxy = |
| c1_node->render_manager()->GetProxyToParent(); |
| auto b1_to_c1_message_filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(c1_proxy); |
| |
| // This will intercept messages sent from B2 to C2, describing C2's viewport |
| // intersection. |
| RenderFrameProxyHost* c2_proxy = |
| c2_node->render_manager()->GetProxyToParent(); |
| auto b2_to_c2_message_filter = |
| std::make_unique<UpdateViewportIntersectionMessageFilter>(c2_proxy); |
| |
| // Running requestAnimationFrame will ensure that any pending IPC's have been |
| // sent by the renderer and received by the browser. |
| auto flush_ipcs = [](FrameTreeNode* node) { |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(node->current_frame_host(), "", "") |
| .error.empty()); |
| }; |
| |
| flush_ipcs(a_node); |
| flush_ipcs(b1_node); |
| flush_ipcs(b2_node); |
| b1_to_c1_message_filter->Clear(); |
| b2_to_c2_message_filter->Clear(); |
| |
| // Now that everything is in a stable, consistent state, we will send viewport |
| // intersection IPC's to B1 and B2 that contain a different |
| // outermost_main_frame_scroll_position, and then verify that each of them |
| // propagates their own value of outermost_main_frame_scroll_position to C1 |
| // and C2, respectively. The IPC code mimics messages that A would send to B1 |
| // and B2. |
| auto b1_intersection_state = b1_node->render_manager() |
| ->GetProxyToParent() |
| ->cross_process_frame_connector() |
| ->intersection_state(); |
| |
| b1_intersection_state.outermost_main_frame_scroll_position.Offset(10, 0); |
| // A change in outermost_main_frame_scroll_position by itself will not cause |
| // B1 to be marked dirty, so we also modify viewport_intersection. |
| b1_intersection_state.viewport_intersection.set_y( |
| b1_intersection_state.viewport_intersection.y() + 7); |
| b1_intersection_state.viewport_intersection.set_height( |
| b1_intersection_state.viewport_intersection.height() - 7); |
| |
| ForceUpdateViewportIntersection(b1_node, b1_intersection_state); |
| |
| auto b2_intersection_state = b2_node->render_manager() |
| ->GetProxyToParent() |
| ->cross_process_frame_connector() |
| ->intersection_state(); |
| |
| b2_intersection_state.outermost_main_frame_scroll_position.Offset(20, 0); |
| b2_intersection_state.viewport_intersection.set_y( |
| b2_intersection_state.viewport_intersection.y() + 7); |
| b2_intersection_state.viewport_intersection.set_height( |
| b2_intersection_state.viewport_intersection.height() - 7); |
| |
| ForceUpdateViewportIntersection(b2_node, b2_intersection_state); |
| |
| // Once IPC's have been flushed to the C frames, we should see conflicting |
| // values for outermost_main_frame_scroll_position. |
| flush_ipcs(b1_node); |
| flush_ipcs(b2_node); |
| ASSERT_TRUE(b1_to_c1_message_filter->MessageReceived()); |
| ASSERT_TRUE(b2_to_c2_message_filter->MessageReceived()); |
| EXPECT_EQ(b1_to_c1_message_filter->GetIntersectionState() |
| ->outermost_main_frame_scroll_position, |
| gfx::Point(10, 0)); |
| EXPECT_EQ(b2_to_c2_message_filter->GetIntersectionState() |
| ->outermost_main_frame_scroll_position, |
| gfx::Point(20, 0)); |
| b1_to_c1_message_filter->Clear(); |
| b2_to_c2_message_filter->Clear(); |
| |
| // If we scroll the main frame, it should propagate IPC's which re-synchronize |
| // the values for all child frames. |
| ASSERT_TRUE(EvalJsAfterLifecycleUpdate(a_node->current_frame_host(), |
| "window.scrollTo(0, 5)", "") |
| .error.empty()); |
| flush_ipcs(b1_node); |
| flush_ipcs(b2_node); |
| ASSERT_TRUE(b1_to_c1_message_filter->MessageReceived()); |
| ASSERT_TRUE(b2_to_c2_message_filter->MessageReceived()); |
| |
| // Window scroll offset will be scaled by device scale factor |
| const float device_scale_factor = a_node->render_manager() |
| ->GetRenderWidgetHostView() |
| ->GetDeviceScaleFactor(); |
| float expected_y = device_scale_factor * 5.0; |
| EXPECT_NEAR(b1_to_c1_message_filter->GetIntersectionState() |
| ->outermost_main_frame_scroll_position.y(), |
| expected_y, 1.f); |
| EXPECT_NEAR(b2_to_c2_message_filter->GetIntersectionState() |
| ->outermost_main_frame_scroll_position.y(), |
| expected_y, 1.f); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| FrameViewportIntersectionTestAggregate) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b,c,a,b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Each immediate child is sized to 100% width and 75% height. |
| LayoutNonRecursiveForTestingViewportIntersection(shell()->web_contents()); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| |
| // Child 2 does not intersect, but shares widget with the main frame. |
| FrameTreeNode* node = root->child_at(2); |
| RenderProcessHostPriorityClient::Priority priority = |
| node->current_frame_host()->GetRenderWidgetHost()->GetPriority(); |
| EXPECT_TRUE(priority.intersects_viewport); |
| EXPECT_TRUE( |
| node->current_frame_host()->GetProcess()->GetIntersectsViewport()); |
| |
| // Child 3 does not intersect, but shares a process with child 0. |
| node = root->child_at(3); |
| priority = node->current_frame_host()->GetRenderWidgetHost()->GetPriority(); |
| EXPECT_FALSE(priority.intersects_viewport); |
| EXPECT_TRUE( |
| node->current_frame_host()->GetProcess()->GetIntersectsViewport()); |
| } |
| |
| // Tests that when a non-root frame in an iframe, performs a RAF to emulate a |
| // scroll, that metrics are reported. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ScrollByRAF) { |
| base::HistogramTester histogram_tester; |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(b))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| " +--Site B -- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| // Layout all three frames, so that the animation has a region to mark dirty. |
| LayoutNonRecursiveForTestingViewportIntersection(root->current_frame_host()); |
| LayoutNonRecursiveForTestingViewportIntersection( |
| root->child_at(0)->current_frame_host()); |
| LayoutNonRecursiveForTestingViewportIntersection( |
| root->child_at(0)->child_at(0)->current_frame_host()); |
| |
| // Add a div to the nested iframe, so that it can be animated. |
| RenderFrameSubmissionObserver frame_observer(root->child_at(0)->child_at(0)); |
| std::string addContent(R"( |
| var d = document.createElement('div'); |
| d.id = 'animationtarget'; |
| d.innerHTML = 'Hey Listen!'; |
| document.body.appendChild(d); |
| )"); |
| ASSERT_TRUE( |
| EvalJsAfterLifecycleUpdate( |
| root->child_at(0)->child_at(0)->current_frame_host(), "", addContent) |
| .error.empty()); |
| frame_observer.WaitForAnyFrameSubmission(); |
| |
| // Fetch the initial metrics, as adding a div can incidentally trigger RAF |
| // metrics. |
| FetchHistogramsFromChildProcesses(); |
| auto initial_samples = histogram_tester.GetAllSamples( |
| "Graphics.Smoothness.PercentDroppedFrames3.MainThread.RAF"); |
| ASSERT_EQ(initial_samples.size(), 0u); |
| |
| const int pre_scroll_frame_count = frame_observer.render_frame_count(); |
| |
| // Run a RAF that takes more than one frame, as metrics due to not track |
| // frames where WillBeginMainFrame occurs before it is triggered. Subsequent |
| // RAFs in the sequence will be measured. |
| std::string scrollByRAF(R"( |
| var offset = 0; |
| function run() { |
| let child = document.getElementById("animationtarget"); |
| var rect = child.getBoundingClientRect(); |
| child.style = 'transform: translateY(' + parseInt(offset)+'px);'; |
| offset += 1; |
| requestAnimationFrame(run); |
| } |
| run(); |
| )"); |
| ASSERT_TRUE( |
| EvalJsAfterLifecycleUpdate( |
| root->child_at(0)->child_at(0)->current_frame_host(), scrollByRAF, "") |
| .error.empty()); |
| |
| // There will have been one frame before the RAF sequence. The minimum for |
| // reporting if 100 frames, however we need to wait at least one extra frame. |
| // On Android the animation begins during the initial call to |
| // EvalJsAfterLifecycleUpdate. However on Linux the first translate is not |
| // applied until the subsequent frame. So we wait for the minimum, then verify |
| // afterwards. |
| const int kExpectedNumberFrames = 101 + pre_scroll_frame_count; |
| while (frame_observer.render_frame_count() < kExpectedNumberFrames) |
| frame_observer.WaitForAnyFrameSubmission(); |
| |
| // We now wait for FrameSequenceTracker to time out in order for it to report. |
| // This will occur once the minimum 100 frames have been produced, and 5s have |
| // passed. If the test times out then the bug is back. |
| while (histogram_tester |
| .GetAllSamples( |
| "Graphics.Smoothness.PercentDroppedFrames3.MainThread.RAF") |
| .empty()) { |
| frame_observer.WaitForAnyFrameSubmission(); |
| FetchHistogramsFromChildProcesses(); |
| } |
| } |
| |
| // Make sure that when a relevant feature of the main frame changes, e.g. the |
| // frame width, that the browser is notified. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, TextAutosizerPageInfo) { |
| UpdateTextAutosizerInfoProxyObserver update_text_autosizer_info_observer; |
| |
| blink::web_pref::WebPreferences prefs = |
| web_contents()->GetOrCreateWebPreferences(); |
| prefs.text_autosizing_enabled = true; |
| |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| web_contents()->SetWebPreferences(prefs); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1U, root->child_count()); |
| FrameTreeNode* b_child = root->child_at(0); |
| |
| blink::mojom::TextAutosizerPageInfo received_page_info; |
| auto interceptor = std::make_unique<TextAutosizerPageInfoInterceptor>( |
| web_contents()->GetPrimaryMainFrame()); |
| #if BUILDFLAG(IS_ANDROID) |
| prefs.device_scale_adjustment += 0.05f; |
| // Change the device scale adjustment to trigger a RemotePageInfo update. |
| web_contents()->SetWebPreferences(prefs); |
| // Make sure we receive a ViewHostMsg from the main frame's renderer. |
| interceptor->WaitForPageInfo(absl::optional<int>(), |
| prefs.device_scale_adjustment); |
| // Make sure the correct page message is sent to the child. |
| base::RunLoop().RunUntilIdle(); |
| received_page_info = interceptor->GetTextAutosizerPageInfo(); |
| EXPECT_EQ(prefs.device_scale_adjustment, |
| received_page_info.device_scale_adjustment); |
| #else |
| // Resize the main frame, then wait to observe that the RemotePageInfo message |
| // arrives. |
| auto* view = web_contents()->GetRenderWidgetHostView(); |
| gfx::Rect old_bounds = view->GetViewBounds(); |
| gfx::Rect new_bounds( |
| old_bounds.origin(), |
| gfx::Size(old_bounds.width() - 20, old_bounds.height() - 20)); |
| |
| view->SetBounds(new_bounds); |
| // Make sure we receive a ViewHostMsg from the main frame's renderer. |
| interceptor->WaitForPageInfo(new_bounds.width(), absl::optional<float>()); |
| // Make sure the correct page message is sent to the child. |
| base::RunLoop().RunUntilIdle(); |
| received_page_info = interceptor->GetTextAutosizerPageInfo(); |
| EXPECT_EQ(new_bounds.width(), received_page_info.main_frame_width); |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // Dynamically create a new, cross-process frame to test sending the cached |
| // TextAutosizerPageInfo. |
| |
| GURL c_url = embedded_test_server()->GetURL("c.com", "/title1.html"); |
| // The following is a hack so we can get an IPC watcher connected to the |
| // RenderProcessHost for C before the `blink::WebView` is created for it, and |
| // the TextAutosizerPageInfo IPC is sent to it. |
| scoped_refptr<SiteInstance> c_site = |
| web_contents()->GetSiteInstance()->GetRelatedSiteInstance(c_url); |
| // Force creation of a render process for c's SiteInstance, this will get |
| // used when we dynamically create the new frame. |
| auto* c_rph = static_cast<RenderProcessHostImpl*>(c_site->GetProcess()); |
| ASSERT_TRUE(c_rph); |
| ASSERT_NE(c_rph, root->current_frame_host()->GetProcess()); |
| ASSERT_NE(c_rph, b_child->current_frame_host()->GetProcess()); |
| |
| // Create the subframe now. |
| std::string create_frame_script = base::StringPrintf( |
| "var new_iframe = document.createElement('iframe');" |
| "new_iframe.src = '%s';" |
| "document.body.appendChild(new_iframe);", |
| c_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(root, create_frame_script)); |
| ASSERT_EQ(2U, root->child_count()); |
| |
| // Ensure IPC is sent. |
| base::RunLoop().RunUntilIdle(); |
| blink::mojom::TextAutosizerPageInfo page_info_sent_to_remote_main_frames = |
| update_text_autosizer_info_observer.TextAutosizerPageInfo( |
| web_contents() |
| ->GetRenderManager() |
| ->GetAllProxyHostsForTesting() |
| .begin() |
| ->second.get()); |
| |
| EXPECT_EQ(received_page_info.main_frame_width, |
| page_info_sent_to_remote_main_frames.main_frame_width); |
| EXPECT_EQ(received_page_info.main_frame_layout_width, |
| page_info_sent_to_remote_main_frames.main_frame_layout_width); |
| EXPECT_EQ(received_page_info.device_scale_adjustment, |
| page_info_sent_to_remote_main_frames.device_scale_adjustment); |
| } |
| |
| // Test that the physical backing size and view bounds for a scaled out-of- |
| // process iframe are set and updated correctly. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| CompositorViewportPixelSizeTest) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_scaled_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* parent_iframe_node = root->child_at(0); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site A ------- proxies for B\n" |
| " +--Site B -- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0); |
| RenderFrameProxyHost* proxy_to_parent = |
| nested_iframe_node->render_manager()->GetProxyToParent(); |
| CrossProcessFrameConnector* connector = |
| proxy_to_parent->cross_process_frame_connector(); |
| RenderWidgetHostViewBase* rwhv_nested = |
| static_cast<RenderWidgetHostViewBase*>( |
| nested_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| |
| RenderFrameSubmissionObserver frame_observer(nested_iframe_node); |
| frame_observer.WaitForMetadataChange(); |
| |
| // Verify that applying a CSS scale transform does not impact the size of the |
| // content of the nested iframe. |
| // The screen_space_rect_in_dip may be off by 1 due to rounding. There is no |
| // good way to avoid this due to various device-scale-factor. (e.g. when |
| // dsf=3.375, ceil(round(50 * 3.375) / 3.375) = 51. Thus, we allow the screen |
| // size in dip to be off by 1 here. |
| EXPECT_NEAR(50, connector->rect_in_parent_view_in_dip().size().width(), 1); |
| EXPECT_NEAR(50, connector->rect_in_parent_view_in_dip().size().height(), 1); |
| EXPECT_EQ(gfx::Size(100, 100), rwhv_nested->GetViewBounds().size()); |
| EXPECT_EQ(gfx::Size(100, 100), connector->local_frame_size_in_dip()); |
| EXPECT_EQ(connector->local_frame_size_in_pixels(), |
| rwhv_nested->GetCompositorViewportPixelSize()); |
| } |
| |
| // Verify an OOPIF resize handler doesn't fire immediately after load without |
| // the frame having been resized. See https://crbug.com/826457. |
| // TODO(crbug.com/1278038): Test is very flaky on many platforms. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_NoResizeAfterIframeLoad) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| FrameTreeNode* iframe = root->child_at(0); |
| GURL site_url = |
| embedded_test_server()->GetURL("b.com", "/page_with_resize_handler.html"); |
| EXPECT_TRUE(NavigateToURLFromRenderer(iframe, site_url)); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Should be zero because the iframe only has its initial size from parent. |
| EXPECT_EQ(0, EvalJs(iframe->current_frame_host(), "resize_count;")); |
| } |
| |
| // Test that the view bounds for an out-of-process iframe are set and updated |
| // correctly, including accounting for local frame offsets in the parent and |
| // scroll positions. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ViewBoundsInNestedFrameTest) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| RenderWidgetHostViewBase* rwhv_root = static_cast<RenderWidgetHostViewBase*>( |
| root->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* parent_iframe_node = root->child_at(0); |
| GURL site_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(parent_iframe_node, site_url)); |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site A ------- proxies for B\n" |
| " +--Site B -- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0); |
| RenderWidgetHostViewBase* rwhv_nested = |
| static_cast<RenderWidgetHostViewBase*>( |
| nested_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| WaitForHitTestData(nested_iframe_node->current_frame_host()); |
| |
| float scale_factor = |
| frame_observer.LastRenderFrameMetadata().page_scale_factor; |
| |
| // Get the view bounds of the nested iframe, which should account for the |
| // relative offset of its direct parent within the root frame. |
| gfx::Rect bounds = rwhv_nested->GetViewBounds(); |
| |
| RenderFrameProxyHost* parent_iframe_proxy = |
| nested_iframe_node->render_manager()->GetProxyToParent(); |
| auto interceptor = std::make_unique<SynchronizeVisualPropertiesInterceptor>( |
| parent_iframe_proxy); |
| |
| // Scroll the parent frame downward to verify that the child rect gets updated |
| // correctly. |
| blink::WebMouseWheelEvent scroll_event( |
| blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| |
| scroll_event.SetPositionInWidget( |
| std::floor((bounds.x() - rwhv_root->GetViewBounds().x() - 5) * |
| scale_factor), |
| std::floor((bounds.y() - rwhv_root->GetViewBounds().y() - 5) * |
| scale_factor)); |
| scroll_event.delta_x = 0.0f; |
| scroll_event.delta_y = -30.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_root->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| interceptor->WaitForRect(); |
| |
| // The precise amount of scroll for the first view position update is not |
| // deterministic, so this simply verifies that the OOPIF moved from its |
| // earlier position. |
| gfx::Rect update_rect = interceptor->last_rect(); |
| EXPECT_LT(update_rect.y(), bounds.y() - rwhv_root->GetViewBounds().y()); |
| } |
| |
| // Verify that "scrolling" property on frame elements propagates to child frames |
| // correctly. |
| // Does not work on android since android has scrollbars overlaid. |
| // TODO(bokan): Pretty soon most/all platforms will use overlay scrollbars. This |
| // test should find a better way to check for scrollability. crbug.com/662196. |
| // Flaky on Linux. crbug.com/790929. |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) |
| #define MAYBE_FrameOwnerPropertiesPropagationScrolling \ |
| DISABLED_FrameOwnerPropertiesPropagationScrolling |
| #else |
| #define MAYBE_FrameOwnerPropertiesPropagationScrolling \ |
| FrameOwnerPropertiesPropagationScrolling |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| MAYBE_FrameOwnerPropertiesPropagationScrolling) { |
| #if BUILDFLAG(IS_MAC) |
| ui::test::ScopedPreferredScrollerStyle scroller_style_override(false); |
| #endif |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_owner_properties_scrolling.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1u, root->child_count()); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* child = root->child_at(0); |
| |
| // If the available client width within the iframe is smaller than the |
| // frame element's width, we assume there's a scrollbar. |
| // Also note that just comparing clientHeight and scrollHeight of the frame's |
| // document will not work. |
| auto has_scrollbar = [](RenderFrameHostImpl* rfh) { |
| int client_width = EvalJs(rfh, "document.body.clientWidth").ExtractInt(); |
| const int kFrameElementWidth = 200; |
| return client_width < kFrameElementWidth; |
| }; |
| |
| auto set_scrolling_property = [](RenderFrameHostImpl* parent_rfh, |
| const std::string& value) { |
| EXPECT_TRUE(ExecJs( |
| parent_rfh, |
| base::StringPrintf("document.getElementById('child-1').setAttribute(" |
| " 'scrolling', '%s');", |
| value.c_str()))); |
| }; |
| |
| // Run the test over variety of parent/child cases. |
| GURL urls[] = {// Remote to remote. |
| embedded_test_server()->GetURL("c.com", "/tall_page.html"), |
| // Remote to local. |
| embedded_test_server()->GetURL("a.com", "/tall_page.html"), |
| // Local to remote. |
| embedded_test_server()->GetURL("b.com", "/tall_page.html")}; |
| const std::string scrolling_values[] = {"yes", "auto", "no"}; |
| |
| for (const auto& scrolling_value : scrolling_values) { |
| bool expect_scrollbar = scrolling_value != "no"; |
| set_scrolling_property(root->current_frame_host(), scrolling_value); |
| for (const auto& url : urls) { |
| EXPECT_TRUE(NavigateToURLFromRenderer(child, url)); |
| EXPECT_EQ(expect_scrollbar, has_scrollbar(child->current_frame_host())); |
| } |
| } |
| } |
| |
| // Verify that "marginwidth" and "marginheight" properties on frame elements |
| // propagate to child frames correctly. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| FrameOwnerPropertiesPropagationMargin) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_owner_properties_margin.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1u, root->child_count()); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* child = root->child_at(0); |
| |
| EXPECT_EQ("10", EvalJs(child, "document.body.getAttribute('marginwidth');")); |
| EXPECT_EQ("50", EvalJs(child, "document.body.getAttribute('marginheight');")); |
| |
| // Run the test over variety of parent/child cases. |
| GURL urls[] = {// Remote to remote. |
| embedded_test_server()->GetURL("c.com", "/title2.html"), |
| // Remote to local. |
| embedded_test_server()->GetURL("a.com", "/title1.html"), |
| // Local to remote. |
| embedded_test_server()->GetURL("b.com", "/title2.html")}; |
| |
| int current_margin_width = 15; |
| int current_margin_height = 25; |
| |
| // Before each navigation, we change the marginwidth and marginheight |
| // properties of the frame. We then check whether those properties are applied |
| // correctly after the navigation has completed. |
| for (const auto& url : urls) { |
| // Change marginwidth and marginheight before navigating. |
| EXPECT_TRUE(ExecJs( |
| root, |
| base::StringPrintf("var child = document.getElementById('child-1');" |
| "child.setAttribute('marginwidth', '%d');" |
| "child.setAttribute('marginheight', '%d');", |
| current_margin_width, current_margin_height))); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(child, url)); |
| |
| EXPECT_EQ(base::NumberToString(current_margin_width), |
| EvalJs(child, "document.body.getAttribute('marginwidth');")); |
| EXPECT_EQ(base::NumberToString(current_margin_height), |
| EvalJs(child, "document.body.getAttribute('marginheight');")); |
| |
| current_margin_width += 5; |
| current_margin_height += 10; |
| } |
| } |
| |
| // Verify that "csp" property on frame elements propagates to child frames |
| // correctly. See https://crbug.com/647588 |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| FrameOwnerPropertiesPropagationCSP) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_owner_properties_csp.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // It is safe to obtain the root frame tree node here, as it doesn't change. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1u, root->child_count()); |
| |
| // The document in the iframe is blocked by CSPEE. An error page is loaded, it |
| // stays in the process of the main document. |
| EXPECT_EQ( |
| " Site A\n" |
| " +--Site A\n" |
| "Where A = http://a.com/", |
| DepictFrameTree(root)); |
| |
| FrameTreeNode* child = root->child_at(0); |
| |
| EXPECT_EQ( |
| "object-src \'none\'", |
| EvalJs(root, "document.getElementById('child-1').getAttribute('csp');")); |
| |
| // Run the test over variety of parent/child cases. |
| struct { |
| std::string csp_value; |
| GURL url; |
| bool should_block; |
| } testCases[]{ |
| // Remote to remote. |
| {"default-src a.com", |
| embedded_test_server()->GetURL("c.com", "/title2.html"), true}, |
| // Remote to local. |
| {"default-src b.com", |
| embedded_test_server()->GetURL("a.com", "/title1.html"), false}, |
| // Local to remote. |
| {"img-src c.com", embedded_test_server()->GetURL("b.com", "/title2.html"), |
| true}, |
| }; |
| |
| // Before each navigation, we change the csp property of the frame. |
| // We then check whether that property is applied |
| // correctly after the navigation has completed. |
| for (const auto& testCase : testCases) { |
| // Change csp before navigating. |
| EXPECT_TRUE(ExecJs( |
| root, |
| base::StringPrintf("document.getElementById('child-1').setAttribute(" |
| " 'csp', '%s');", |
| testCase.csp_value.c_str()))); |
| |
| NavigateFrameToURL(child, testCase.url); |
| EXPECT_EQ(testCase.csp_value, child->csp_attribute()->header->header_value); |
| // TODO(amalika): add checks that the CSP replication takes effect |
| |
| const url::Origin child_origin = |
| child->current_frame_host()->GetLastCommittedOrigin(); |
| |
| EXPECT_EQ(testCase.should_block, child_origin.opaque()); |
| EXPECT_EQ(url::Origin::Create(testCase.url.DeprecatedGetOriginAsURL()) |
| .GetTupleOrPrecursorTupleIfOpaque(), |
| child_origin.GetTupleOrPrecursorTupleIfOpaque()); |
| } |
| } |
| |
| // This test verifies that changing the CSS visibility of a cross-origin |
| // <iframe> is forwarded to its corresponding RenderWidgetHost and all other |
| // RenderWidgetHosts corresponding to the nested cross-origin frame. |
| // TODO(crbug.com/1363740): Flaky on mac, linux-lacros, android. |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS_LACROS) |
| #define MAYBE_CSSVisibilityChanged DISABLED_CSSVisibilityChanged |
| #else |
| #define MAYBE_CSSVisibilityChanged CSSVisibilityChanged |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, MAYBE_CSSVisibilityChanged) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(b(c(d(d(a))))))")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| // Find all child RenderWidgetHosts. |
| std::vector<RenderWidgetHostImpl*> child_widget_hosts; |
| FrameTreeNode* first_cross_process_child = |
| web_contents()->GetPrimaryFrameTree().root()->child_at(0); |
| for (auto* ftn : web_contents()->GetPrimaryFrameTree().SubtreeNodes( |
| first_cross_process_child)) { |
| RenderFrameHostImpl* frame_host = ftn->current_frame_host(); |
| if (!frame_host->is_local_root()) |
| continue; |
| |
| child_widget_hosts.push_back(frame_host->GetRenderWidgetHost()); |
| } |
| |
| // Ignoring the root, there is exactly 4 local roots and hence 5 |
| // RenderWidgetHosts on the page. |
| EXPECT_EQ(4U, child_widget_hosts.size()); |
| |
| // Initially all the RenderWidgetHosts should be visible. |
| for (size_t index = 0; index < child_widget_hosts.size(); ++index) { |
| EXPECT_FALSE(child_widget_hosts[index]->is_hidden()) |
| << "The RWH at distance " << index + 1U |
| << " from root RWH should not be hidden."; |
| } |
| |
| std::string show_script = |
| "document.querySelector('iframe').style.visibility = 'visible';"; |
| std::string hide_script = |
| "document.querySelector('iframe').style.visibility = 'hidden';"; |
| |
| // Define observers for notifications about hiding child RenderWidgetHosts. |
| std::vector<std::unique_ptr<RenderWidgetHostVisibilityObserver>> |
| hide_widget_host_observers(child_widget_hosts.size()); |
| for (size_t index = 0U; index < child_widget_hosts.size(); ++index) { |
| hide_widget_host_observers[index] = |
| std::make_unique<RenderWidgetHostVisibilityObserver>( |
| child_widget_hosts[index], false); |
| } |
| |
| EXPECT_TRUE(ExecJs(shell(), hide_script)); |
| for (size_t index = 0U; index < child_widget_hosts.size(); ++index) { |
| EXPECT_TRUE(hide_widget_host_observers[index]->WaitUntilSatisfied()) |
| << "Expected RenderWidgetHost at distance " << index + 1U |
| << " from root RenderWidgetHost to become hidden."; |
| } |
| |
| // Define observers for notifications about showing child RenderWidgetHosts. |
| std::vector<std::unique_ptr<RenderWidgetHostVisibilityObserver>> |
| show_widget_host_observers(child_widget_hosts.size()); |
| for (size_t index = 0U; index < child_widget_hosts.size(); ++index) { |
| show_widget_host_observers[index] = |
| std::make_unique<RenderWidgetHostVisibilityObserver>( |
| child_widget_hosts[index], true); |
| } |
| |
| EXPECT_TRUE(ExecJs(shell(), show_script)); |
| for (size_t index = 0U; index < child_widget_hosts.size(); ++index) { |
| EXPECT_TRUE(show_widget_host_observers[index]->WaitUntilSatisfied()) |
| << "Expected RenderWidgetHost at distance " << index + 1U |
| << " from root RenderWidgetHost to become shown."; |
| } |
| } |
| |
| // This test verifies that hiding an OOPIF in CSS will stop generating |
| // compositor frames for the OOPIF and any nested OOPIFs inside it. This holds |
| // even when the whole page is shown. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| HiddenOOPIFWillNotGenerateCompositorFrames) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_two_frames.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| ASSERT_EQ(shell()->web_contents()->GetLastCommittedURL(), main_url); |
| |
| GURL cross_site_url_b = |
| embedded_test_server()->GetURL("b.com", "/counter.html"); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url_b)); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), cross_site_url_b)); |
| |
| // Now inject code in the first frame to create a nested OOPIF. |
| RenderFrameHostCreatedObserver new_frame_created_observer( |
| shell()->web_contents(), 1); |
| ASSERT_TRUE( |
| ExecJs(root->child_at(0)->current_frame_host(), |
| "document.body.appendChild(document.createElement('iframe'));")); |
| new_frame_created_observer.Wait(); |
| |
| GURL cross_site_url_a = |
| embedded_test_server()->GetURL("a.com", "/counter.html"); |
| |
| // Navigate the nested frame. |
| TestFrameNavigationObserver observer(root->child_at(0)->child_at(0)); |
| ASSERT_TRUE(ExecJs(root->child_at(0)->current_frame_host(), |
| JsReplace("document.querySelector('iframe').src = $1", |
| cross_site_url_a))); |
| observer.Wait(); |
| |
| RenderWidgetHostViewChildFrame* first_child_view = |
| static_cast<RenderWidgetHostViewChildFrame*>( |
| root->child_at(0)->current_frame_host()->GetView()); |
| RenderWidgetHostViewChildFrame* second_child_view = |
| static_cast<RenderWidgetHostViewChildFrame*>( |
| root->child_at(1)->current_frame_host()->GetView()); |
| RenderWidgetHostViewChildFrame* nested_child_view = |
| static_cast<RenderWidgetHostViewChildFrame*>( |
| root->child_at(0)->child_at(0)->current_frame_host()->GetView()); |
| |
| RenderFrameSubmissionObserver first_frame_counter( |
| first_child_view->host_->render_frame_metadata_provider()); |
| RenderFrameSubmissionObserver second_frame_counter( |
| second_child_view->host_->render_frame_metadata_provider()); |
| RenderFrameSubmissionObserver third_frame_counter( |
| nested_child_view->host_->render_frame_metadata_provider()); |
| |
| const int kFrameCountLimit = 20; |
| |
| // Wait for a minimum number of compositor frames for the second frame. |
| while (second_frame_counter.render_frame_count() < kFrameCountLimit) |
| second_frame_counter.WaitForAnyFrameSubmission(); |
| ASSERT_LE(kFrameCountLimit, second_frame_counter.render_frame_count()); |
| |
| // Now make sure all frames have roughly the counter value in the sense that |
| // no counter value is more than twice any other. |
| float ratio = static_cast<float>(first_frame_counter.render_frame_count()) / |
| static_cast<float>(second_frame_counter.render_frame_count()); |
| EXPECT_GT(2.5f, ratio + 1 / ratio) << "Ratio is: " << ratio; |
| |
| ratio = static_cast<float>(first_frame_counter.render_frame_count()) / |
| static_cast<float>(third_frame_counter.render_frame_count()); |
| EXPECT_GT(2.5f, ratio + 1 / ratio) << "Ratio is: " << ratio; |
| |
| // Make sure all views can become visible. |
| EXPECT_TRUE(first_child_view->CanBecomeVisible()); |
| EXPECT_TRUE(second_child_view->CanBecomeVisible()); |
| EXPECT_TRUE(nested_child_view->CanBecomeVisible()); |
| |
| // Hide the first frame and wait for the notification to be posted by its |
| // RenderWidgetHost. |
| RenderWidgetHostVisibilityObserver hide_observer( |
| root->child_at(0)->current_frame_host()->GetRenderWidgetHost(), false); |
| |
| // Hide the first frame. |
| ASSERT_TRUE(ExecJs( |
| shell(), |
| "document.getElementsByName('frame1')[0].style.visibility = 'hidden'")); |
| ASSERT_TRUE(hide_observer.WaitUntilSatisfied()); |
| EXPECT_TRUE(first_child_view->FrameConnectorForTesting()->IsHidden()); |
| |
| // Verify that only the second view can become visible now. |
| EXPECT_FALSE(first_child_view->CanBecomeVisible()); |
| EXPECT_TRUE(second_child_view->CanBecomeVisible()); |
| EXPECT_FALSE(nested_child_view->CanBecomeVisible()); |
| |
| // Now hide and show the WebContents (to simulate a tab switch). |
| shell()->web_contents()->WasHidden(); |
| shell()->web_contents()->WasShown(); |
| |
| first_frame_counter.ResetCounter(); |
| second_frame_counter.ResetCounter(); |
| third_frame_counter.ResetCounter(); |
| |
| // We expect the second counter to keep running. |
| while (second_frame_counter.render_frame_count() < kFrameCountLimit) |
| second_frame_counter.WaitForAnyFrameSubmission(); |
| ASSERT_LT(kFrameCountLimit, second_frame_counter.render_frame_count() + 1); |
| |
| // Verify that the counter for other two frames did not count much. |
| ratio = static_cast<float>(first_frame_counter.render_frame_count()) / |
| static_cast<float>(second_frame_counter.render_frame_count()); |
| EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio; |
| |
| ratio = static_cast<float>(third_frame_counter.render_frame_count()) / |
| static_cast<float>(second_frame_counter.render_frame_count()); |
| EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio; |
| } |
| |
| // This test verifies that navigating a hidden OOPIF to cross-origin will not |
| // lead to creating compositor frames for the new OOPIF renderer. |
| IN_PROC_BROWSER_TEST_P( |
| SitePerProcessBrowserTest, |
| HiddenOOPIFWillNotGenerateCompositorFramesAfterNavigation) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_two_frames.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| ASSERT_EQ(shell()->web_contents()->GetLastCommittedURL(), main_url); |
| |
| GURL cross_site_url_b = |
| embedded_test_server()->GetURL("b.com", "/counter.html"); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url_b)); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), cross_site_url_b)); |
| |
| // Hide the first frame and wait for the notification to be posted by its |
| // RenderWidgetHost. |
| RenderWidgetHostVisibilityObserver hide_observer( |
| root->child_at(0)->current_frame_host()->GetRenderWidgetHost(), false); |
| |
| // Hide the first frame. |
| ASSERT_TRUE(ExecJs( |
| shell(), |
| "document.getElementsByName('frame1')[0].style.visibility = 'hidden'")); |
| ASSERT_TRUE(hide_observer.WaitUntilSatisfied()); |
| |
| // Now navigate the first frame to another OOPIF process. |
| TestFrameNavigationObserver navigation_observer( |
| root->child_at(0)->current_frame_host()); |
| GURL cross_site_url_c = |
| embedded_test_server()->GetURL("c.com", "/counter.html"); |
| ASSERT_TRUE( |
| ExecJs(web_contents(), |
| JsReplace("document.getElementsByName('frame1')[0].src = $1", |
| cross_site_url_c))); |
| navigation_observer.Wait(); |
| |
| // Now investigate compositor frame creation. |
| RenderWidgetHostViewChildFrame* first_child_view = |
| static_cast<RenderWidgetHostViewChildFrame*>( |
| root->child_at(0)->current_frame_host()->GetView()); |
| |
| RenderWidgetHostViewChildFrame* second_child_view = |
| static_cast<RenderWidgetHostViewChildFrame*>( |
| root->child_at(1)->current_frame_host()->GetView()); |
| |
| EXPECT_FALSE(first_child_view->CanBecomeVisible()); |
| |
| RenderFrameSubmissionObserver first_frame_counter( |
| first_child_view->host_->render_frame_metadata_provider()); |
| RenderFrameSubmissionObserver second_frame_counter( |
| second_child_view->host_->render_frame_metadata_provider()); |
| |
| const int kFrameCountLimit = 20; |
| |
| // Wait for a certain number of swapped compositor frames generated for the |
| // second child view. During the same interval the first frame should not have |
| // swapped any compositor frames. |
| while (second_frame_counter.render_frame_count() < kFrameCountLimit) |
| second_frame_counter.WaitForAnyFrameSubmission(); |
| ASSERT_LT(kFrameCountLimit, second_frame_counter.render_frame_count() + 1); |
| |
| float ratio = static_cast<float>(first_frame_counter.render_frame_count()) / |
| static_cast<float>(second_frame_counter.render_frame_count()); |
| EXPECT_GT(0.5f, ratio) << "Ratio is: " << ratio; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ScreenCoordinates) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child = root->child_at(0); |
| |
| const char* properties[] = {"screenX", "screenY", "outerWidth", |
| "outerHeight"}; |
| |
| for (const char* property : properties) { |
| std::string script = base::StringPrintf("window.%s;", property); |
| int root_value = EvalJs(root, script).ExtractInt(); |
| int child_value = EvalJs(child, script).ExtractInt(); |
| EXPECT_EQ(root_value, child_value); |
| } |
| } |
| |
| // Tests that an out-of-process iframe receives the visibilitychange event. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, VisibilityChange) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| EXPECT_TRUE( |
| ExecJs(root->child_at(0), |
| "var event_fired = 0;\n" |
| "document.addEventListener('visibilitychange',\n" |
| " function() { event_fired++; });\n")); |
| |
| shell()->web_contents()->WasHidden(); |
| |
| EXPECT_EQ(1, EvalJs(root->child_at(0), "event_fired")); |
| |
| shell()->web_contents()->WasShown(); |
| |
| EXPECT_EQ(2, EvalJs(root->child_at(0), "event_fired")); |
| } |
| |
| // This test verifies that the main-frame's page scale factor propagates to |
| // the compositor layertrees in each of the child processes. |
| // Flaky on all platforms: https://crbug.com/1116774 |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_PageScaleFactorPropagatesToOOPIFs) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(c),d)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(2u, root->child_count()); |
| FrameTreeNode* child_b = root->child_at(0); |
| FrameTreeNode* child_c = root->child_at(1); |
| ASSERT_EQ(1U, child_b->child_count()); |
| FrameTreeNode* child_d = child_b->child_at(0); |
| |
| ASSERT_TRUE(child_b); |
| ASSERT_TRUE(child_c); |
| ASSERT_TRUE(child_d); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C D\n" |
| " |--Site B ------- proxies for A C D\n" |
| " | +--Site C -- proxies for A B D\n" |
| " +--Site D ------- proxies for A B C\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/\n" |
| " C = http://c.com/\n" |
| " D = http://d.com/", |
| DepictFrameTree(root)); |
| |
| RenderFrameSubmissionObserver observer_a(root); |
| RenderFrameSubmissionObserver observer_b(child_b); |
| RenderFrameSubmissionObserver observer_c(child_c); |
| RenderFrameSubmissionObserver observer_d(child_d); |
| |
| // Monitor visual sync messages coming from the mainframe to make sure |
| // |is_pinch_gesture_active| goes true during the pinch gesture. |
| RenderFrameProxyHost* root_proxy_host = |
| child_d->render_manager()->GetProxyToParent(); |
| auto interceptor_mainframe = |
| std::make_unique<SynchronizeVisualPropertiesInterceptor>(root_proxy_host); |
| |
| // Monitor frame sync messages coming from child_b as it will need to |
| // relay them to child_d. |
| RenderFrameProxyHost* child_b_proxy_host = |
| child_c->render_manager()->GetProxyToParent(); |
| auto interceptor_child_b = |
| std::make_unique<SynchronizeVisualPropertiesInterceptor>( |
| child_b_proxy_host); |
| |
| // We need to observe a root frame submission to pick up the initial page |
| // scale factor. |
| observer_a.WaitForAnyFrameSubmission(); |
| |
| const float kPageScaleDelta = 2.f; |
| // On desktop systems we expect |current_page_scale| to be 1.f, but on |
| // Android it will typically be less than 1.f, and may take on arbitrary |
| // values. |
| float current_page_scale = |
| observer_a.LastRenderFrameMetadata().page_scale_factor; |
| float target_page_scale = current_page_scale * kPageScaleDelta; |
| |
| SyntheticPinchGestureParams params; |
| auto* host = static_cast<RenderWidgetHostImpl*>( |
| root->current_frame_host()->GetRenderWidgetHost()); |
| gfx::Rect bounds(host->GetView()->GetViewBounds().size()); |
| // The synthetic gesture code expects a location in root-view coordinates. |
| params.anchor = gfx::PointF(bounds.CenterPoint()); |
| // In SyntheticPinchGestureParams, |scale_factor| is really a delta. |
| params.scale_factor = kPageScaleDelta; |
| #if BUILDFLAG(IS_MAC) |
| auto synthetic_pinch_gesture = |
| std::make_unique<SyntheticTouchpadPinchGesture>(params); |
| #else |
| auto synthetic_pinch_gesture = |
| std::make_unique<SyntheticTouchscreenPinchGesture>(params); |
| #endif |
| |
| // Send pinch gesture and verify we receive the ack. |
| InputEventAckWaiter ack_waiter(host, |
| blink::WebInputEvent::Type::kGesturePinchEnd); |
| host->QueueSyntheticGesture( |
| std::move(synthetic_pinch_gesture), |
| base::BindOnce([](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result); |
| })); |
| ack_waiter.Wait(); |
| |
| // Make sure all the page scale values behave as expected. |
| const float kScaleTolerance = 0.1f; |
| observer_a.WaitForPageScaleFactor(target_page_scale, kScaleTolerance); |
| observer_b.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance); |
| observer_c.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance); |
| observer_d.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance); |
| |
| // The change in |is_pinch_gesture_active| that signals the end of the pinch |
| // gesture will occur sometime after the ack for GesturePinchEnd, so we need |
| // to wait for it from each renderer. If it's never seen, the test fails by |
| // timing out. |
| interceptor_mainframe->WaitForPinchGestureEnd(); |
| interceptor_child_b->WaitForPinchGestureEnd(); |
| } |
| |
| // Test that the compositing scale factor for an out-of-process iframe are set |
| // and updated correctly, including accounting for all intermediate transforms. |
| // TODO(crbug.com/1164391): Flaky test. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_CompositingScaleFactorInNestedFrameTest) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_scaled_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| ASSERT_EQ(1U, root->child_count()); |
| FrameTreeNode* child_b = root->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_b, embedded_test_server()->GetURL( |
| "b.com", "/frame_tree/page_with_transformed_iframe.html"))); |
| |
| ASSERT_EQ(1U, child_b->child_count()); |
| FrameTreeNode* child_c = child_b->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_c, embedded_test_server()->GetURL( |
| "c.com", "/frame_tree/page_with_scaled_frame.html"))); |
| |
| ASSERT_EQ(1U, child_c->child_count()); |
| FrameTreeNode* child_d = child_c->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_d, embedded_test_server()->GetURL("d.com", "/simple_page.html"))); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B C D\n" |
| " +--Site B ------- proxies for A C D\n" |
| " +--Site C -- proxies for A B D\n" |
| " +--Site D -- proxies for A B C\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/\n" |
| " C = http://c.com/\n" |
| " D = http://d.com/", |
| DepictFrameTree(root)); |
| |
| // Wait for b.com's frame to have its compositing scale factor set to 0.5, |
| // which is the scale factor for b.com's iframe element in the main frame. |
| while (true) { |
| auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost(); |
| absl::optional<blink::VisualProperties> properties = |
| rwh_b->LastComputedVisualProperties(); |
| if (properties && cc::MathUtil::IsFloatNearlyTheSame( |
| properties->compositing_scale_factor, 0.5f)) { |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Wait for c.com's frame to have its compositing scale factor set to 0.5, |
| // which is the accumulated scale factor of c.com to the main frame obtained |
| // by multiplying the scale factor of c.com's iframe element (1 since |
| // transform is rotation only without scale) with the scale factor of its |
| // parent frame b.com (0.5). |
| while (true) { |
| auto* rwh_c = child_c->current_frame_host()->GetRenderWidgetHost(); |
| absl::optional<blink::VisualProperties> properties = |
| rwh_c->LastComputedVisualProperties(); |
| if (properties && cc::MathUtil::IsFloatNearlyTheSame( |
| properties->compositing_scale_factor, 0.5f)) { |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Wait for d.com's frame to have its compositing scale factor set to 0.25, |
| // which is the accumulated scale factor of d.com to the main frame obtained |
| // by combining the scale factor of d.com's iframe element (0.5) with the |
| // scale factor of its parent d.com (0.5). |
| while (true) { |
| auto* rwh_d = child_d->current_frame_host()->GetRenderWidgetHost(); |
| absl::optional<blink::VisualProperties> properties = |
| rwh_d->LastComputedVisualProperties(); |
| if (properties && cc::MathUtil::IsFloatNearlyTheSame( |
| properties->compositing_scale_factor, 0.25f)) { |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| // Test that the compositing scale factor for an out-of-process iframe is set |
| // to a non-zero value even if intermediate CSS transform has zero scale. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| CompositingScaleFactorWithZeroScaleTransform) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_scaled_frame.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| ASSERT_EQ(1U, root->child_count()); |
| FrameTreeNode* child_b = root->child_at(0); |
| |
| EXPECT_TRUE(NavigateToURLFromRenderer( |
| child_b, |
| embedded_test_server()->GetURL("b.com", "/frame_tree/simple_page.html"))); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| // Wait for b.com's frame to have its compositing scale factor set to 0.5, |
| // which is the scale factor for b.com's iframe element in the main frame. |
| while (true) { |
| auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost(); |
| absl::optional<blink::VisualProperties> properties = |
| rwh_b->LastComputedVisualProperties(); |
| if (properties && cc::MathUtil::IsFloatNearlyTheSame( |
| properties->compositing_scale_factor, 0.5f)) { |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Set iframe transform scale to 0. |
| EXPECT_TRUE( |
| EvalJs(root->current_frame_host(), |
| "document.querySelector('iframe').style.transform = 'scale(0)'") |
| .error.empty()); |
| |
| // Wait for b.com frame's compositing scale factor to change, and check that |
| // the final value is non-zero. |
| while (true) { |
| auto* rwh_b = child_b->current_frame_host()->GetRenderWidgetHost(); |
| absl::optional<blink::VisualProperties> properties = |
| rwh_b->LastComputedVisualProperties(); |
| if (properties && !cc::MathUtil::IsFloatNearlyTheSame( |
| properties->compositing_scale_factor, 0.5f)) { |
| EXPECT_GT(properties->compositing_scale_factor, 0.0f); |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| // Check that when a frame changes a subframe's size twice and then sends a |
| // postMessage to the subframe, the subframe's onmessage handler sees the new |
| // size. In particular, ensure that the postMessage won't get reordered with |
| // the second resize, which might be throttled if the first resize is still in |
| // progress. See https://crbug.com/828529. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| ResizeAndCrossProcessPostMessagePreserveOrder) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| |
| // Add an onmessage handler to the subframe to send back its width. |
| EXPECT_TRUE(ExecJs(root->child_at(0), |
| WaitForMessageScript("document.body.clientWidth"))); |
| |
| // Drop the visual properties ACKs from the child renderer. To do this, |
| // unsubscribe the child's RenderWidgetHost from its |
| // RenderFrameMetadataProvider, which ensures that |
| // DidUpdateVisualProperties() won't be called on it, and the ACK won't be |
| // reset. This simulates that the ACK for the first resize below does not |
| // arrive before the second resize IPC arrives from the |
| // parent, and that the second resize IPC early-exits in |
| // SynchronizeVisualProperties() due to the pending visual properties ACK. |
| RenderWidgetHostImpl* rwh = |
| root->child_at(0)->current_frame_host()->GetRenderWidgetHost(); |
| rwh->render_frame_metadata_provider_.RemoveObserver(rwh); |
| |
| // Now, resize the subframe twice from the main frame and send it a |
| // postMessage. The postMessage handler should see the second updated size. |
| EXPECT_TRUE(ExecJs(root, R"( |
| var f = document.querySelector('iframe'); |
| f.width = 500; |
| f.offsetTop; // force layout; this sends a resize IPC for width of 500. |
| f.width = 700; |
| f.offsetTop; // force layout; this sends a resize IPC for width of 700. |
| f.contentWindow.postMessage('foo', '*');)")); |
| EXPECT_EQ(700, EvalJs(root->child_at(0), "onMessagePromise")); |
| } |
| |
| // This test verifies that when scrolling an OOPIF in a pinched-zoomed page, |
| // that the scroll-delta matches the distance between TouchStart/End as seen |
| // by the oopif, i.e. the oopif content 'sticks' to the finger during scrolling. |
| // The relation is not exact, but should be close. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| ScrollOopifInPinchZoomedPage) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| ASSERT_EQ(1u, root->child_count()); |
| FrameTreeNode* child = root->child_at(0); |
| ASSERT_TRUE(child); |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://a.com/\n" |
| " B = http://b.com/", |
| DepictFrameTree(root)); |
| |
| // Make B scrollable. The call to document.write will erase the html inside |
| // the OOPIF, leaving just a vertical column of 'Hello's. |
| std::string script = |
| "var s = '<div>Hello</div>\\n';\n" |
| "document.write(s.repeat(200));"; |
| EXPECT_TRUE(ExecJs(child, script)); |
| |
| RenderFrameSubmissionObserver observer_a(root); |
| RenderFrameSubmissionObserver observer_b(child); |
| |
| // We need to observe a root frame submission to pick up the initial page |
| // scale factor. |
| observer_a.WaitForAnyFrameSubmission(); |
| |
| const float kPageScaleDelta = 2.f; |
| // On desktop systems we expect |current_page_scale| to be 1.f, but on |
| // Android it will typically be less than 1.f, and may take on arbitrary |
| // values. |
| float original_page_scale = |
| observer_a.LastRenderFrameMetadata().page_scale_factor; |
| float target_page_scale = original_page_scale * kPageScaleDelta; |
| |
| SyntheticPinchGestureParams params; |
| auto* host = static_cast<RenderWidgetHostImpl*>( |
| root->current_frame_host()->GetRenderWidgetHost()); |
| RenderWidgetHostViewBase* root_view = host->GetView(); |
| RenderWidgetHostViewBase* child_view = |
| static_cast<RenderWidgetHostImpl*>( |
| child->current_frame_host()->GetRenderWidgetHost()) |
| ->GetView(); |
| gfx::Rect bounds(root_view->GetViewBounds().size()); |
| // The synthetic gesture code expects a location in root-view coordinates. |
| params.anchor = gfx::PointF(bounds.CenterPoint().x(), 70.f); |
| // In SyntheticPinchGestureParams, |scale_factor| is really a delta. |
| params.scale_factor = kPageScaleDelta; |
| #if BUILDFLAG(IS_MAC) |
| auto synthetic_pinch_gesture = |
| std::make_unique<SyntheticTouchpadPinchGesture>(params); |
| #else |
| auto synthetic_pinch_gesture = |
| std::make_unique<SyntheticTouchscreenPinchGesture>(params); |
| #endif |
| |
| // Send pinch gesture and verify we receive the ack. |
| { |
| InputEventAckWaiter ack_waiter( |
| host, blink::WebInputEvent::Type::kGesturePinchEnd); |
| host->QueueSyntheticGesture( |
| std::move(synthetic_pinch_gesture), |
| base::BindOnce([](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result); |
| })); |
| ack_waiter.Wait(); |
| } |
| |
| // Make sure all the page scale values behave as expected. |
| const float kScaleTolerance = 0.07f; |
| observer_a.WaitForPageScaleFactor(target_page_scale, kScaleTolerance); |
| observer_b.WaitForExternalPageScaleFactor(target_page_scale, kScaleTolerance); |
| float final_page_scale = |
| observer_a.LastRenderFrameMetadata().page_scale_factor; |
| |
| // Verify scroll position of OOPIF. |
| float initial_child_scroll = EvalJs(child, "window.scrollY").ExtractDouble(); |
| |
| // Send touch-initiated gesture scroll sequence to OOPIF. |
| // TODO(wjmaclean): GetViewBounds() is broken for OOPIFs when PSF != 1.f, so |
| // we calculate it manually. This will need to be update when GetViewBounds() |
| // in RenderWidgetHostViewBase is fixed. See https://crbug.com/928825. |
| auto child_bounds = child_view->GetViewBounds(); |
| gfx::PointF child_upper_left = |
| child_view->TransformPointToRootCoordSpaceF(gfx::PointF(0, 0)); |
| gfx::PointF child_lower_right = child_view->TransformPointToRootCoordSpaceF( |
| gfx::PointF(child_bounds.width(), child_bounds.height())); |
| gfx::PointF scroll_start_location_in_screen = |
| gfx::PointF((child_upper_left.x() + child_lower_right.x()) / 2.f, |
| child_lower_right.y() - 10); |
| const float kScrollDelta = 100.f; |
| gfx::PointF scroll_end_location_in_screen = |
| scroll_start_location_in_screen + gfx::Vector2dF(0, -kScrollDelta); |
| |
| // Create touch move sequence with discrete touch moves. Include a brief |
| // pause at the end to avoid the scroll flinging. |
| std::string actions_template = R"HTML( |
| [{ |
| "source" : "touch", |
| "actions" : [ |
| { "name": "pointerDown", "x": %f, "y": %f}, |
| { "name": "pointerMove", "x": %f, "y": %f}, |
| { "name": "pause", "duration": 300 }, |
| { "name": "pointerUp"} |
| ] |
| }] |
| )HTML"; |
| std::string touch_move_sequence_json = base::StringPrintf( |
| actions_template.c_str(), scroll_start_location_in_screen.x(), |
| scroll_start_location_in_screen.y(), scroll_end_location_in_screen.x(), |
| scroll_end_location_in_screen.y()); |
| auto parsed_json = |
| base::JSONReader::ReadAndReturnValueWithError(touch_move_sequence_json); |
| ASSERT_TRUE(parsed_json.has_value()) << parsed_json.error().message; |
| ActionsParser actions_parser(std::move(*parsed_json)); |
| |
| ASSERT_TRUE(actions_parser.Parse()); |
| auto synthetic_scroll_gesture = |
| SyntheticGesture::Create(actions_parser.gesture_params()); |
| |
| { |
| auto* child_host = static_cast<RenderWidgetHostImpl*>( |
| child->current_frame_host()->GetRenderWidgetHost()); |
| InputEventAckWaiter ack_waiter( |
| child_host, blink::WebInputEvent::Type::kGestureScrollEnd); |
| host->QueueSyntheticGesture( |
| std::move(synthetic_scroll_gesture), |
| base::BindOnce([](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result); |
| })); |
| ack_waiter.Wait(); |
| } |
| |
| // Verify new scroll position of OOPIF, should match touch sequence delta. |
| float expected_scroll_delta = kScrollDelta / final_page_scale; |
| float actual_scroll_delta = |
| EvalJs(child, "window.scrollY").ExtractDouble() - initial_child_scroll; |
| |
| const float kScrollTolerance = 0.2f; |
| EXPECT_GT((1.f + kScrollTolerance) * expected_scroll_delta, |
| actual_scroll_delta); |
| EXPECT_LT((1.f - kScrollTolerance) * expected_scroll_delta, |
| actual_scroll_delta); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| SitePerProcessHighDPIBrowserTest, |
| testing::ValuesIn(RenderDocumentFeatureLevelValues())); |
| } // namespace content |