| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/site_per_process_browsertest.h" |
| |
| #include "base/test/bind.h" |
| #include "base/test/test_timeouts.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/cross_process_frame_connector.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/input/synthetic_smooth_scroll_gesture.h" |
| #include "content/browser/renderer_host/render_frame_proxy_host.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 "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/events/gesture_detection/gesture_configuration.h" |
| #include "ui/events/keycodes/dom/keycode_converter.h" |
| #include "ui/native_theme/native_theme_features.h" |
| |
| namespace content { |
| |
| class ScrollingIntegrationTest : public SitePerProcessBrowserTest { |
| public: |
| ScrollingIntegrationTest() = default; |
| ~ScrollingIntegrationTest() override = default; |
| |
| void DoScroll(const gfx::Point& point, |
| const gfx::Vector2d& distance, |
| content::mojom::GestureSourceType source) { |
| SyntheticSmoothScrollGestureParams params; |
| params.gesture_source_type = source; |
| params.anchor = gfx::PointF(point); |
| params.distances.push_back(-distance); |
| params.granularity = ui::ScrollGranularity::kScrollByPrecisePixel; |
| |
| auto gesture = std::make_unique<SyntheticSmoothScrollGesture>(params); |
| |
| // Runs until we get the SyntheticGestureCompleted callback |
| base::RunLoop run_loop; |
| GetRenderWidgetHostImpl()->QueueSyntheticGesture( |
| std::move(gesture), |
| base::BindLambdaForTesting([&](SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| double GetScrollTop() { |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| return EvalJs(root, "window.scrollY").ExtractDouble(); |
| } |
| |
| void WaitForVerticalScroll() { |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| gfx::PointF default_scroll_offset; |
| while (frame_observer.LastRenderFrameMetadata() |
| .root_scroll_offset.value_or(default_scroll_offset) |
| .y() <= 0) { |
| frame_observer.WaitForMetadataChange(); |
| } |
| } |
| |
| RenderWidgetHostImpl* GetRenderWidgetHostImpl() { |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| return root->current_frame_host()->GetRenderWidgetHost(); |
| } |
| }; |
| |
| // Tests basic scrolling after navigating to a new origin works. Guards against |
| // bugs like https://crbug.com/899234 which are caused by invalid |
| // initialization due to the cross-origin provisional frame swap. |
| IN_PROC_BROWSER_TEST_P(ScrollingIntegrationTest, |
| ScrollAfterCrossOriginNavigation) { |
| // Navigate to the a.com domain first. |
| GURL url_domain_a( |
| embedded_test_server()->GetURL("a.com", "/simple_page.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_domain_a)); |
| |
| // Now navigate to baz.com, this should cause a cross-origin navigation which |
| // will load into a provisional frame and then swap in as a local main frame. |
| // This test ensures all the correct initialization takes place in the |
| // renderer so that a basic scrolling smoke test works. |
| GURL url_domain_b(embedded_test_server()->GetURL( |
| "baz.com", "/scrollable_page_with_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_domain_b)); |
| ASSERT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| { |
| // TODO(bokan): We currently don't have a good way to know when the |
| // compositor's scrolling layers are ready after changes on the main thread. |
| // We wait a timeout but that's really a hack. Fixing is tracked in |
| // https://crbug.com/897520 |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(3000)); |
| run_loop.Run(); |
| } |
| |
| content::mojom::GestureSourceType source; |
| |
| // TODO(bokan): Mac doesn't support touch events and for an unknown reason, |
| // Android doesn't like mouse wheel here. https://crbug.com/897520. |
| #if BUILDFLAG(IS_ANDROID) |
| source = content::mojom::GestureSourceType::kTouchInput; |
| #else |
| source = content::mojom::GestureSourceType::kTouchpadInput; |
| #endif |
| |
| // Perform the scroll (below the iframe), ensure it's correctly processed. |
| DoScroll(gfx::Point(100, 110), gfx::Vector2d(0, 500), source); |
| WaitForVerticalScroll(); |
| EXPECT_GT(GetScrollTop(), 0); |
| } |
| |
| class SitePerProcessScrollAnchorTest : public SitePerProcessBrowserTest { |
| public: |
| SitePerProcessScrollAnchorTest() = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| SitePerProcessBrowserTestBase::SetUpCommandLine(command_line); |
| command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, |
| "ScrollAnchorSerialization"); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessScrollAnchorTest, |
| RemoteToLocalScrollAnchorRestore) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/page_with_samesite_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child = root->child_at(0); |
| |
| GURL frame_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(child, frame_url)); |
| |
| EXPECT_NE(child->current_frame_host()->GetSiteInstance(), |
| root->current_frame_host()->GetSiteInstance()); |
| |
| TestFrameNavigationObserver frame_observer2(child); |
| EXPECT_TRUE(ExecJs(root, "window.history.back()")); |
| frame_observer2.Wait(); |
| |
| EXPECT_EQ(child->current_frame_host()->GetSiteInstance(), |
| root->current_frame_host()->GetSiteInstance()); |
| } |
| |
| class SitePerProcessProgrammaticScrollTest : public SitePerProcessBrowserTest { |
| public: |
| SitePerProcessProgrammaticScrollTest() |
| : kPositiveXYPlane(0, 0, kInfinity, kInfinity) {} |
| |
| SitePerProcessProgrammaticScrollTest( |
| const SitePerProcessProgrammaticScrollTest&) = delete; |
| SitePerProcessProgrammaticScrollTest& operator=( |
| const SitePerProcessProgrammaticScrollTest&) = delete; |
| |
| protected: |
| const size_t kInfinity = 1000000u; |
| const std::string kIframeOutOfViewHTML = "/iframe_out_of_view.html"; |
| const std::string kIframeClippedHTML = "/iframe_clipped.html"; |
| const std::string kInputBoxHTML = "/input_box.html"; |
| const std::string kIframeSelector = "iframe"; |
| const std::string kInputSelector = "input"; |
| const gfx::Rect kPositiveXYPlane; |
| |
| // Waits until the |load| handle is called inside the frame. |
| void WaitForOnLoad(FrameTreeNode* node) { |
| RunCommandAndWaitForResponse(node, "notifyWhenLoaded();", "LOADED"); |
| } |
| |
| void WaitForElementVisible(FrameTreeNode* node, const std::string& sel) { |
| RunCommandAndWaitForResponse( |
| node, |
| base::StringPrintf("notifyWhenVisible(document.querySelector('%s'));", |
| sel.c_str()), |
| "VISIBLE"); |
| } |
| |
| void WaitForViewportToStabilize(FrameTreeNode* node) { |
| RunCommandAndWaitForResponse(node, "notifyWhenViewportStable(0);", |
| "VIEWPORT_STABLE"); |
| } |
| |
| void AddFocusedInputField(FrameTreeNode* node) { |
| ASSERT_TRUE(ExecJs(node, "addFocusedInputField();")); |
| } |
| |
| void SetWindowScroll(FrameTreeNode* node, int x, int y) { |
| ASSERT_TRUE( |
| ExecJs(node, base::StringPrintf("window.scrollTo(%d, %d);", x, y))); |
| } |
| |
| // Helper function to retrieve the bounding client rect of the element |
| // identified by |sel| inside |rfh|. |
| gfx::Rect GetBoundingClientRect(RenderFrameHostImpl* rfh, |
| const std::string& sel) { |
| return GetRectFromString( |
| EvalJs(rfh, JsReplace("rectAsString(document.querySelector($1)." |
| "getBoundingClientRect());", |
| sel)) |
| .ExtractString()); |
| } |
| |
| // Returns a rect representing the current |visualViewport| in the main frame |
| // of |contents|. |
| gfx::Rect GetVisualViewport(FrameTreeNode* node) { |
| return GetRectFromString( |
| EvalJs(node, "rectAsString(visualViewportAsRect());").ExtractString()); |
| } |
| |
| float GetVisualViewportScale(FrameTreeNode* node) { |
| return EvalJs(node, "visualViewport.scale;").ExtractDouble(); |
| } |
| |
| private: |
| void RunCommandAndWaitForResponse(FrameTreeNode* node, |
| const std::string& command, |
| const std::string& response) { |
| ASSERT_EQ(response, |
| EvalJs(node, command, content::EXECUTE_SCRIPT_USE_MANUAL_REPLY)); |
| } |
| |
| gfx::Rect GetRectFromString(const std::string& str) { |
| std::vector<std::string> tokens = base::SplitString( |
| str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| EXPECT_EQ(4U, tokens.size()); |
| double x = 0.0, y = 0.0, width = 0.0, height = 0.0; |
| EXPECT_TRUE(base::StringToDouble(tokens[0], &x)); |
| EXPECT_TRUE(base::StringToDouble(tokens[1], &y)); |
| EXPECT_TRUE(base::StringToDouble(tokens[2], &width)); |
| EXPECT_TRUE(base::StringToDouble(tokens[3], &height)); |
| return {static_cast<int>(x), static_cast<int>(y), static_cast<int>(width), |
| static_cast<int>(height)}; |
| } |
| }; |
| |
| // This test verifies that scrolling an element to view works across OOPIFs. The |
| // testing methodology is based on measuring bounding client rect position of |
| // nested <iframe>'s after the inner-most frame scrolls into view. The |
| // measurements are for two identical pages where one page does not have any |
| // OOPIFs while the other has some nested OOPIFs. |
| // TODO(crbug.com/827431): This test is flaking on all platforms. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessProgrammaticScrollTest, |
| DISABLED_ScrollElementIntoView) { |
| const GURL url_a( |
| embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML)); |
| const GURL url_b( |
| embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML)); |
| const GURL url_c( |
| embedded_test_server()->GetURL("c.com", kIframeOutOfViewHTML)); |
| |
| // Number of <iframe>'s which will not be empty. The actual frame tree has two |
| // more nodes one for root and one for the inner-most empty <iframe>. |
| const size_t kNonEmptyIframesCount = 5; |
| const std::string kScrollIntoViewScript = |
| "document.body.scrollIntoView({'behavior' : 'instant'});"; |
| const int kRectDimensionErrorTolerance = 0; |
| |
| // First, recursively set the |scrollTop| and |scrollLeft| of |document.body| |
| // to its maximum and then navigate the <iframe> to |url_a|. The page will be |
| // structured as a(a(a(a(a(a(a)))))) where the inner-most <iframe> is empty. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| FrameTreeNode* node = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(node); |
| std::vector<gfx::Rect> reference_page_bounds_before_scroll = { |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)}; |
| node = node->child_at(0); |
| for (size_t index = 0; index < kNonEmptyIframesCount; ++index) { |
| EXPECT_TRUE(NavigateToURLFromRenderer(node, url_a)); |
| WaitForOnLoad(node); |
| // Store |document.querySelector('iframe').getBoundingClientRect()|. |
| reference_page_bounds_before_scroll.push_back( |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)); |
| node = node->child_at(0); |
| } |
| // Sanity-check: If the page is setup properly then all the <iframe>s should |
| // be out of view and their bounding rect should not intersect with the |
| // positive XY plane. |
| for (const auto& rect : reference_page_bounds_before_scroll) |
| ASSERT_FALSE(rect.Intersects(kPositiveXYPlane)); |
| // Now scroll the inner-most frame into view. |
| ASSERT_TRUE(ExecJs(node, kScrollIntoViewScript)); |
| // Store current client bounds origins to later compare against those from the |
| // page which contains OOPIFs. |
| node = web_contents()->GetPrimaryFrameTree().root(); |
| std::vector<gfx::Rect> reference_page_bounds_after_scroll = { |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)}; |
| node = node->child_at(0); |
| for (size_t index = 0; index < kNonEmptyIframesCount; ++index) { |
| reference_page_bounds_after_scroll.push_back( |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)); |
| node = node->child_at(0); |
| } |
| |
| // Repeat the same process for the page containing OOPIFs. The page is |
| // structured as b(b(a(c(a(a(a)))))) where the inner-most <iframe> is empty. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| node = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(node); |
| std::vector<gfx::Rect> test_page_bounds_before_scroll = { |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)}; |
| const GURL iframe_urls[] = {url_b, url_a, url_c, url_a, url_a}; |
| node = node->child_at(0); |
| for (const auto& iframe_url : iframe_urls) { |
| EXPECT_TRUE(NavigateToURLFromRenderer(node, iframe_url)); |
| WaitForOnLoad(node); |
| test_page_bounds_before_scroll.push_back( |
| GetBoundingClientRect(node->current_frame_host(), kIframeSelector)); |
| node = node->child_at(0); |
| } |
| // Sanity-check: The bounds should match those from non-OOPIF page. |
| for (size_t index = 0; index < kNonEmptyIframesCount; ++index) { |
| ASSERT_TRUE(test_page_bounds_before_scroll[index].ApproximatelyEqual( |
| reference_page_bounds_before_scroll[index], |
| kRectDimensionErrorTolerance)); |
| } |
| // Scroll the inner most OOPIF. |
| ASSERT_TRUE(ExecJs(node, kScrollIntoViewScript)); |
| // Now traverse the chain bottom to top and verify the bounds match for each |
| // <iframe>. |
| int index = kNonEmptyIframesCount; |
| RenderFrameHostImpl* current_rfh = node->current_frame_host()->GetParent(); |
| while (current_rfh) { |
| gfx::Rect current_bounds = |
| GetBoundingClientRect(current_rfh, kIframeSelector); |
| gfx::Rect reference_bounds = reference_page_bounds_after_scroll[index]; |
| if (current_bounds.ApproximatelyEqual(reference_bounds, |
| kRectDimensionErrorTolerance)) { |
| current_rfh = current_rfh->GetParent(); |
| --index; |
| } else { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| } |
| } |
| } |
| |
| // This test verifies that ScrollFocusedEditableElementIntoView works correctly |
| // for OOPIFs. Essentially, the test verifies that in a similar setup, the |
| // resultant page scale factor is the same for OOPIF and non-OOPIF cases. This |
| // also verifies that in response to the scroll command, the root-layer scrolls |
| // correctly and the <input> is visible in visual viewport. |
| #if BUILDFLAG(IS_ANDROID) |
| // crbug.com/793616 |
| #define MAYBE_ScrollFocusedEditableElementIntoView \ |
| DISABLED_ScrollFocusedEditableElementIntoView |
| #else |
| #define MAYBE_ScrollFocusedEditableElementIntoView \ |
| ScrollFocusedEditableElementIntoView |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessProgrammaticScrollTest, |
| MAYBE_ScrollFocusedEditableElementIntoView) { |
| GURL url_a(embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML)); |
| GURL url_b(embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML)); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // The reason for Android specific code is that |
| // AutoZoomFocusedNodeToLegibleScale is in blink's WebSettings and difficult |
| // to access from here. It so happens that the setting is on for Android. |
| |
| // A lower bound on the ratio of page scale factor after scroll. The actual |
| // value depends on minReadableCaretHeight / caret_bounds.Height(). The page |
| // is setup so caret height is quite small so the expected scale should be |
| // larger than 2.0. |
| float kLowerBoundOnScaleAfterScroll = 2.0; |
| float kEpsilon = 0.1; |
| #endif |
| |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(root); |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), url_a)); |
| WaitForOnLoad(root->child_at(0)); |
| #if BUILDFLAG(IS_ANDROID) |
| float scale_before_scroll_nonoopif = GetVisualViewportScale(root); |
| #endif |
| AddFocusedInputField(root->child_at(0)); |
| // Focusing <input> causes scrollIntoView(). The following line makes sure |
| // that the <iframe> is out of view again. |
| SetWindowScroll(root, 0, 0); |
| ASSERT_FALSE(GetVisualViewport(root).Intersects( |
| GetBoundingClientRect(root->current_frame_host(), kIframeSelector))); |
| root->child_at(0) |
| ->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetFrameWidgetInputHandler() |
| ->ScrollFocusedEditableNodeIntoRect(gfx::Rect()); |
| WaitForElementVisible(root, kIframeSelector); |
| #if BUILDFLAG(IS_ANDROID) |
| float scale_after_scroll_nonoopif = GetVisualViewportScale(root); |
| // Increased scale means zoom triggered correctly. |
| EXPECT_GT(scale_after_scroll_nonoopif - scale_before_scroll_nonoopif, |
| kEpsilon); |
| EXPECT_GT(scale_after_scroll_nonoopif, kLowerBoundOnScaleAfterScroll); |
| #endif |
| |
| // Retry the test on an OOPIF page. |
| Shell* new_shell = CreateBrowser(); |
| ASSERT_TRUE(NavigateToURL(new_shell, url_b)); |
| root = static_cast<WebContentsImpl*>(new_shell->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| WaitForOnLoad(root); |
| #if BUILDFLAG(IS_ANDROID) |
| float scale_before_scroll_oopif = GetVisualViewportScale(root); |
| // Sanity-check: |
| ASSERT_NEAR(scale_before_scroll_oopif, scale_before_scroll_nonoopif, |
| kEpsilon); |
| #endif |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), url_a)); |
| WaitForOnLoad(root->child_at(0)); |
| AddFocusedInputField(root->child_at(0)); |
| SetWindowScroll(root, 0, 0); |
| ASSERT_FALSE(GetVisualViewport(root).Intersects( |
| GetBoundingClientRect(root->current_frame_host(), kIframeSelector))); |
| root->child_at(0) |
| ->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetFrameWidgetInputHandler() |
| ->ScrollFocusedEditableNodeIntoRect(gfx::Rect()); |
| WaitForElementVisible(root, kIframeSelector); |
| #if BUILDFLAG(IS_ANDROID) |
| float scale_after_scroll_oopif = GetVisualViewportScale(root); |
| EXPECT_GT(scale_after_scroll_oopif - scale_before_scroll_oopif, kEpsilon); |
| EXPECT_GT(scale_after_scroll_oopif, kLowerBoundOnScaleAfterScroll); |
| // The scale is based on the caret height and it should be the same in both |
| // OOPIF and non-OOPIF pages. |
| EXPECT_NEAR(scale_after_scroll_oopif, scale_after_scroll_nonoopif, kEpsilon); |
| #endif |
| // Make sure the <input> is at least partly visible in the |visualViewport|. |
| gfx::Rect final_visual_viewport_oopif = GetVisualViewport(root); |
| gfx::Rect iframe_bounds_after_scroll_oopif = |
| GetBoundingClientRect(root->current_frame_host(), kIframeSelector); |
| gfx::Rect input_bounds_after_scroll_oopif = GetBoundingClientRect( |
| root->child_at(0)->current_frame_host(), kInputSelector); |
| input_bounds_after_scroll_oopif += |
| iframe_bounds_after_scroll_oopif.OffsetFromOrigin(); |
| ASSERT_TRUE( |
| final_visual_viewport_oopif.Intersects(input_bounds_after_scroll_oopif)); |
| } |
| |
| // Failing on Android, see crbug.com/1246843 |
| // Flaky on Mac, see crbug.com/1156657 |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_MAC) |
| #define MAYBE_ScrollClippedFocusedEditableElementIntoView \ |
| DISABLED_ScrollClippedFocusedEditableElementIntoView |
| #else |
| #define MAYBE_ScrollClippedFocusedEditableElementIntoView \ |
| ScrollClippedFocusedEditableElementIntoView |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessProgrammaticScrollTest, |
| MAYBE_ScrollClippedFocusedEditableElementIntoView) { |
| GURL url_a(embedded_test_server()->GetURL("a.com", kIframeClippedHTML)); |
| GURL child_url_b(embedded_test_server()->GetURL("b.com", kInputBoxHTML)); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(root); |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), child_url_b)); |
| WaitForOnLoad(root->child_at(0)); |
| |
| SetWindowScroll(root, 0, 0); |
| SetWindowScroll(root->child_at(0), 1000, 2000); |
| |
| float scale_before = GetVisualViewportScale(root); |
| |
| // The input_box page focuses the input box on load. This call should |
| // simulate the scroll into view we do when an input box is tapped. |
| root->child_at(0) |
| ->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetFrameWidgetInputHandler() |
| ->ScrollFocusedEditableNodeIntoRect(gfx::Rect()); |
| |
| // The scroll into view is animated on the compositor. Make sure we wait |
| // until that's completed before testing the rects. |
| WaitForElementVisible(root, kIframeSelector); |
| WaitForViewportToStabilize(root); |
| |
| // These rects are in the coordinate space of the root frame. |
| gfx::Rect visual_viewport_rect = GetVisualViewport(root); |
| gfx::Rect window_rect = |
| GetBoundingClientRect(root->current_frame_host(), ":root"); |
| gfx::Rect iframe_rect = |
| GetBoundingClientRect(root->current_frame_host(), "iframe"); |
| gfx::Rect clip_rect = |
| GetBoundingClientRect(root->current_frame_host(), "#clip"); |
| |
| // This is in the coordinate space of the iframe, we'll add the iframe offset |
| // after to put it into the root frame's coordinate space. |
| gfx::Rect input_rect = |
| GetBoundingClientRect(root->child_at(0)->current_frame_host(), "input"); |
| |
| // Make sure the input rect is visible in the iframe. |
| EXPECT_TRUE(gfx::Rect(iframe_rect.size()).Intersects(input_rect)) |
| << "Input box [" << input_rect.ToString() << "] isn't visible in iframe [" |
| << gfx::Rect(iframe_rect.size()).ToString() << "]"; |
| |
| input_rect += iframe_rect.OffsetFromOrigin(); |
| |
| // Make sure the input rect is visible through the clipping layer. |
| EXPECT_TRUE(clip_rect.Intersects(input_rect)) |
| << "Input box [" << input_rect.ToString() << "] isn't scrolled into view " |
| << "of the clipping layer [" << clip_rect.ToString() << "]"; |
| |
| // And finally, it should be visible in the layout and visual viewports. |
| EXPECT_TRUE(window_rect.Intersects(input_rect)) |
| << "Input box [" << input_rect.ToString() << "] isn't visible in the " |
| << "layout viewport [" << window_rect.ToString() << "]"; |
| EXPECT_TRUE(visual_viewport_rect.Intersects(input_rect)) |
| << "Input box [" << input_rect.ToString() << "] isn't visible in the " |
| << "visual viewport [" << visual_viewport_rect.ToString() << "]"; |
| |
| float scale_after = GetVisualViewportScale(root); |
| |
| // Make sure we still zoom in on the input box on platforms that zoom into the |
| // focused editable. |
| #if BUILDFLAG(IS_ANDROID) |
| EXPECT_GT(scale_after, scale_before); |
| #else |
| EXPECT_FLOAT_EQ(scale_after, scale_before); |
| #endif |
| } |
| |
| IN_PROC_BROWSER_TEST_P(SitePerProcessProgrammaticScrollTest, |
| ScrolledOutOfView) { |
| GURL main_frame( |
| embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML)); |
| GURL child_url_b( |
| embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML)); |
| |
| // This will set up the page frame tree as A(B()). |
| ASSERT_TRUE(NavigateToURL(shell(), main_frame)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(root); |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), child_url_b)); |
| WaitForOnLoad(root->child_at(0)); |
| |
| FrameTreeNode* nested_iframe_node = root->child_at(0); |
| RenderFrameProxyHost* proxy_to_parent = |
| nested_iframe_node->render_manager()->GetProxyToParent(); |
| CrossProcessFrameConnector* connector = |
| proxy_to_parent->cross_process_frame_connector(); |
| |
| while (blink::mojom::FrameVisibility::kRenderedOutOfViewport != |
| connector->visibility()) { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| } |
| } |
| |
| // This test verifies that smooth scrolling works correctly inside nested OOPIFs |
| // which are same origin with the parent. Note that since the frame tree has |
| // a A(B(A1())) structure, if and A1 and A2 shared the same |
| // SmoothScrollSequencer, then this test would time out or at best be flaky with |
| // random time outs. See https://crbug.com/865446 for more context. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessProgrammaticScrollTest, |
| SmoothScrollInNestedSameProcessOOPIF) { |
| GURL main_frame( |
| embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML)); |
| GURL child_url_b( |
| embedded_test_server()->GetURL("b.com", kIframeOutOfViewHTML)); |
| GURL same_origin( |
| embedded_test_server()->GetURL("a.com", kIframeOutOfViewHTML)); |
| |
| // This will set up the page frame tree as A(B(A1(A2()))) where A1 is later |
| // asked to scroll the <iframe> element of A2 into view. The important bit |
| // here is that the inner frame A1 is recursively scrolling (smoothly) an |
| // element inside its document into view (A2's origin is irrelevant here). |
| ASSERT_TRUE(NavigateToURL(shell(), main_frame)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| WaitForOnLoad(root); |
| EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), child_url_b)); |
| WaitForOnLoad(root->child_at(0)); |
| auto* nested_ftn = root->child_at(0)->child_at(0); |
| EXPECT_TRUE(NavigateToURLFromRenderer(nested_ftn, same_origin)); |
| WaitForOnLoad(nested_ftn); |
| |
| // *Smoothly* scroll the inner most frame into view. |
| ASSERT_TRUE(ExecJs( |
| nested_ftn, |
| "document.querySelector('iframe').scrollIntoView({behavior: 'smooth'})")); |
| WaitForElementVisible(root, kIframeSelector); |
| WaitForElementVisible(root->child_at(0), kIframeSelector); |
| WaitForElementVisible(nested_ftn, kIframeSelector); |
| } |
| |
| class ScrollObserver : public RenderWidgetHost::InputEventObserver { |
| public: |
| ScrollObserver(double delta_x, double delta_y) { Reset(delta_x, delta_y); } |
| ~ScrollObserver() override = default; |
| |
| ScrollObserver(const ScrollObserver&) = delete; |
| ScrollObserver& operator=(const ScrollObserver&) = delete; |
| |
| void OnInputEvent(const blink::WebInputEvent& event) override { |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate) { |
| blink::WebGestureEvent received_update = |
| *static_cast<const blink::WebGestureEvent*>(&event); |
| remaining_delta_x_ -= received_update.data.scroll_update.delta_x; |
| remaining_delta_y_ -= received_update.data.scroll_update.delta_y; |
| } else if (event.GetType() == |
| blink::WebInputEvent::Type::kGestureScrollEnd) { |
| if (run_loop_->running()) |
| run_loop_->Quit(); |
| DCHECK_EQ(0, remaining_delta_x_); |
| DCHECK_EQ(0, remaining_delta_y_); |
| scroll_end_received_ = true; |
| } |
| } |
| |
| void Wait() { |
| if (!scroll_end_received_) |
| run_loop_->Run(); |
| } |
| |
| void Reset(double delta_x, double delta_y) { |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| remaining_delta_x_ = delta_x; |
| remaining_delta_y_ = delta_y; |
| scroll_end_received_ = false; |
| } |
| |
| private: |
| std::unique_ptr<base::RunLoop> run_loop_; |
| double remaining_delta_x_; |
| double remaining_delta_y_; |
| bool scroll_end_received_; |
| }; |
| |
| // Android: crbug.com/825629 |
| // NDEBUG: crbug.com/1063045 |
| #if BUILDFLAG(IS_ANDROID) || defined(NDEBUG) |
| #define MAYBE_ScrollBubblingFromNestedOOPIFTest \ |
| DISABLED_ScrollBubblingFromNestedOOPIFTest |
| #else |
| #define MAYBE_ScrollBubblingFromNestedOOPIFTest \ |
| ScrollBubblingFromNestedOOPIFTest |
| #endif |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| MAYBE_ScrollBubblingFromNestedOOPIFTest) { |
| ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms( |
| 0); |
| GURL main_url(embedded_test_server()->GetURL( |
| "/frame_tree/page_with_positioned_nested_frames.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| |
| // 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()); |
| |
| 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_EQ(site_url, parent_iframe_node->current_url()); |
| |
| FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0); |
| GURL nested_site_url( |
| embedded_test_server()->GetURL("baz.com", "/title1.html")); |
| EXPECT_EQ(nested_site_url, nested_iframe_node->current_url()); |
| |
| RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>( |
| root->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| |
| RenderWidgetHostViewBase* rwhv_nested = |
| static_cast<RenderWidgetHostViewBase*>( |
| nested_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| |
| WaitForHitTestData(nested_iframe_node->current_frame_host()); |
| |
| InputEventAckWaiter ack_observer( |
| root->current_frame_host()->GetRenderWidgetHost(), |
| blink::WebInputEvent::Type::kGestureScrollBegin); |
| |
| std::unique_ptr<ScrollObserver> scroll_observer; |
| |
| // All GSU events will be wrapped between a single GSB-GSE pair. The expected |
| // delta value is equal to summation of all scroll update deltas. |
| scroll_observer = std::make_unique<ScrollObserver>(0, 15); |
| |
| root->current_frame_host()->GetRenderWidgetHost()->AddInputEventObserver( |
| scroll_observer.get()); |
| |
| // Now scroll the nested frame upward, this must bubble all the way up to the |
| // root. |
| blink::WebMouseWheelEvent scroll_event( |
| blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| gfx::Rect bounds = rwhv_nested->GetViewBounds(); |
| float scale_factor = |
| frame_observer.LastRenderFrameMetadata().page_scale_factor; |
| scroll_event.SetPositionInWidget( |
| std::ceil((bounds.x() - root_view->GetViewBounds().x() + 10) * |
| scale_factor), |
| std::ceil((bounds.y() - root_view->GetViewBounds().y() + 10) * |
| scale_factor)); |
| scroll_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel; |
| scroll_event.delta_x = 0.0f; |
| scroll_event.delta_y = 5.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| ack_observer.Wait(); |
| |
| // Send 10 wheel events with delta_y = 1 to the nested oopif. |
| scroll_event.delta_y = 1.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseChanged; |
| for (int i = 0; i < 10; i++) |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // Send a wheel end event to complete the scrolling sequence. |
| scroll_event.delta_y = 0.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded; |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| scroll_observer->Wait(); |
| } |
| |
| // Tests that scrolling bubbles from an oopif if its source body has |
| // "overflow:hidden" style. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| ScrollBubblingFromOOPIFWithBodyOverflowHidden) { |
| GURL url_domain_a(embedded_test_server()->GetURL( |
| "a.com", "/scrollable_page_with_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_domain_a)); |
| RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| |
| FrameTreeNode* iframe_node = root->child_at(0); |
| GURL url_domain_b( |
| embedded_test_server()->GetURL("b.com", "/body_overflow_hidden.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(iframe_node, url_domain_b)); |
| WaitForHitTestData(iframe_node->current_frame_host()); |
| |
| RenderWidgetHostViewBase* root_view = static_cast<RenderWidgetHostViewBase*>( |
| root->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| |
| RenderWidgetHostViewBase* child_view = static_cast<RenderWidgetHostViewBase*>( |
| iframe_node->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| |
| ScrollObserver scroll_observer(0, -5); |
| base::ScopedObservation<RenderWidgetHostImpl, |
| RenderWidgetHost::InputEventObserver, |
| &RenderWidgetHostImpl::AddInputEventObserver, |
| &RenderWidgetHostImpl::RemoveInputEventObserver> |
| scroll_observation_(&scroll_observer); |
| scroll_observation_.Observe( |
| root->current_frame_host()->GetRenderWidgetHost()); |
| |
| // Now scroll the nested frame downward, this must bubble to the root since |
| // the iframe source body is not scrollable. |
| blink::WebMouseWheelEvent scroll_event( |
| blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| gfx::Rect bounds = child_view->GetViewBounds(); |
| float scale_factor = |
| frame_observer.LastRenderFrameMetadata().page_scale_factor; |
| scroll_event.SetPositionInWidget( |
| std::ceil((bounds.x() - root_view->GetViewBounds().x() + 10) * |
| scale_factor), |
| std::ceil((bounds.y() - root_view->GetViewBounds().y() + 10) * |
| scale_factor)); |
| scroll_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel; |
| scroll_event.delta_x = 0.0f; |
| scroll_event.delta_y = -5.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| child_view->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // Send a wheel end event to complete the scrolling sequence. |
| scroll_event.delta_y = 0.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded; |
| child_view->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| scroll_observer.Wait(); |
| } |
| |
| // This class intercepts RenderFrameProxyHost creations, and creates an |
| // SynchronizeVisualPropertiesInterceptor to intercept the message of |
| // SynchronizeVisualProperties. We may not use them all but we need to create |
| // the interceptors as soon as the RenderFrameProxyHost is created so we don't |
| // miss any messages. |
| class ScrollBubblingProxyObserver : RenderFrameProxyHost::TestObserver { |
| public: |
| ScrollBubblingProxyObserver() { |
| RenderFrameProxyHost::SetObserverForTesting(this); |
| } |
| |
| // We don't need to set an empty callback to |
| // RenderFrameProxyHost::Set[Created|Deleted]CallbackForTesting because we |
| // already bound callbacks using a weak ptr. |
| ~ScrollBubblingProxyObserver() override { |
| RenderFrameProxyHost::SetObserverForTesting(nullptr); |
| } |
| |
| SynchronizeVisualPropertiesInterceptor* interceptor( |
| RenderFrameProxyHost* proxy) { |
| return interceptors_.find(proxy)->second.get(); |
| } |
| |
| private: |
| void OnCreated(RenderFrameProxyHost* proxy_host) override { |
| interceptors_.emplace( |
| proxy_host, |
| std::make_unique<SynchronizeVisualPropertiesInterceptor>(proxy_host)); |
| } |
| |
| void OnDeleted(RenderFrameProxyHost* proxy_host) override { |
| // RenderFrameProxyHost can be deleted before the test is finished. In such |
| // case, |interceptors_| should remove the mapped interceptor to avoid a |
| // dangling pointer issue when it's destroyed. |
| interceptors_.erase(proxy_host); |
| } |
| |
| std::map<RenderFrameProxyHost*, |
| std::unique_ptr<SynchronizeVisualPropertiesInterceptor>> |
| interceptors_; |
| }; |
| |
| // Test that scrolling a nested out-of-process iframe bubbles unused scroll |
| // delta to a parent frame. |
| // Flaky on all platforms: https://crbug.com/1148741 |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| DISABLED_ScrollBubblingFromOOPIFTest) { |
| ScrollBubblingProxyObserver scroll_bubbling_proxy_observer; |
| |
| ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms( |
| 0); |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(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 = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| ASSERT_EQ(1U, root->child_count()); |
| |
| FrameTreeNode* parent_iframe_node = root->child_at(0); |
| |
| GURL site_url(embedded_test_server()->GetURL( |
| "b.com", "/frame_tree/page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(parent_iframe_node, site_url)); |
| |
| InputEventAckWaiter ack_observer( |
| parent_iframe_node->current_frame_host()->GetRenderWidgetHost(), |
| blink::WebInputEvent::Type::kGestureScrollEnd); |
| |
| // Navigate the nested frame to a page large enough to have scrollbars. |
| FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0); |
| GURL nested_site_url( |
| embedded_test_server()->GetURL("baz.com", "/tall_page.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(nested_iframe_node, nested_site_url)); |
| |
| // This test uses the position of the nested iframe within the parent iframe |
| // to infer the scroll position of the parent. |
| // SynchronizeVisualPropertiesInterceptor catches updates to the position in |
| // order to avoid busy waiting. It gets created early to catch the initial |
| // rects from the navigation. |
| RenderFrameProxyHost* parent_iframe_proxy = |
| nested_iframe_node->render_manager()->GetProxyToParent(); |
| |
| NavigateFrameToURL(nested_iframe_node, nested_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://a.com/\n" |
| " B = http://b.com/\n" |
| " C = http://baz.com/", |
| DepictFrameTree(root)); |
| |
| RenderWidgetHostViewBase* rwhv_parent = |
| static_cast<RenderWidgetHostViewBase*>( |
| parent_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| |
| RenderWidgetHostViewBase* rwhv_nested = |
| static_cast<RenderWidgetHostViewBase*>( |
| nested_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| |
| WaitForHitTestData(parent_iframe_node->current_frame_host()); |
| |
| auto* interceptor = |
| scroll_bubbling_proxy_observer.interceptor(parent_iframe_proxy); |
| |
| // Save the original offset as a point of reference. |
| interceptor->WaitForRect(); |
| gfx::Rect update_rect = interceptor->last_rect(); |
| int initial_y = update_rect.y(); |
| interceptor->ResetRectRunLoop(); |
| |
| // Scroll the parent frame downward. |
| blink::WebMouseWheelEvent scroll_event( |
| blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| scroll_event.SetPositionInWidget(1, 1); |
| // Use precise pixels to keep these events off the animated scroll pathways, |
| // which currently break this test. |
| // https://bugs.chromium.org/p/chromium/issues/detail?id=710513 |
| scroll_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel; |
| scroll_event.delta_x = 0.0f; |
| scroll_event.delta_y = -5.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // The event router sends wheel events of a single scroll sequence to the |
| // target under the first wheel event. Send a wheel end event to the current |
| // target view before sending a wheel event to a different one. |
| scroll_event.delta_y = 0.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded; |
| scroll_event.dispatch_type = |
| blink::WebInputEvent::DispatchType::kEventNonBlocking; |
| rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // Ensure that the view position is propagated to the child properly. |
| interceptor->WaitForRect(); |
| update_rect = interceptor->last_rect(); |
| EXPECT_LT(update_rect.y(), initial_y); |
| interceptor->ResetRectRunLoop(); |
| ack_observer.Reset(); |
| |
| // Now scroll the nested frame upward, which should bubble to the parent. |
| // The upscroll exceeds the amount that the frame was initially scrolled |
| // down to account for rounding. |
| scroll_event.delta_y = 6.0f; |
| scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| interceptor->WaitForRect(); |
| // This loop isn't great, but it accounts for the possibility of multiple |
| // incremental updates happening as a result of the scroll animation. |
| // A failure condition of this test is that the loop might not terminate |
| // due to bubbling not working properly. If the overscroll bubbles to the |
| // parent iframe then the nested frame's y coord will return to its |
| // initial position. |
| update_rect = interceptor->last_rect(); |
| while (update_rect.y() > initial_y) { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| update_rect = interceptor->last_rect(); |
| } |
| |
| // The event router sends wheel events of a single scroll sequence to the |
| // target under the first wheel event. Send a wheel end event to the current |
| // target view before sending a wheel event to a different one. |
| scroll_event.delta_y = 0.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded; |
| scroll_event.dispatch_type = |
| blink::WebInputEvent::DispatchType::kEventNonBlocking; |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| interceptor->ResetRectRunLoop(); |
| // Once we've sent a wheel to the nested iframe that we expect to turn into |
| // a bubbling scroll, we need to delay to make sure the GestureScrollBegin |
| // from this new scroll doesn't hit the RenderWidgetHostImpl before the |
| // GestureScrollEnd bubbled from the child. |
| // This timing only seems to be needed for CrOS, but we'll enable it on |
| // all platforms just to lessen the possibility of tests being flakey |
| // on non-CrOS platforms. |
| ack_observer.Wait(); |
| |
| // Scroll the parent down again in order to test scroll bubbling from |
| // gestures. |
| scroll_event.delta_y = -5.0f; |
| scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // The event router sends wheel events of a single scroll sequence to the |
| // target under the first wheel event. Send a wheel end event to the current |
| // target view before sending a wheel event to a different one. |
| scroll_event.delta_y = 0.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseEnded; |
| scroll_event.dispatch_type = |
| blink::WebInputEvent::DispatchType::kEventNonBlocking; |
| rwhv_parent->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| |
| // Ensure ensuing offset change is received, and then reset the interceptor. |
| interceptor->WaitForRect(); |
| interceptor->ResetRectRunLoop(); |
| |
| // Scroll down the nested iframe via gesture. This requires 3 separate input |
| // events. |
| blink::WebGestureEvent gesture_event( |
| blink::WebGestureEvent::Type::kGestureScrollBegin, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests(), |
| blink::WebGestureDevice::kTouchpad); |
| gesture_event.SetPositionInWidget(gfx::PointF(1, 1)); |
| gesture_event.data.scroll_begin.delta_x_hint = 0.0f; |
| gesture_event.data.scroll_begin.delta_y_hint = 6.0f; |
| rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event); |
| |
| gesture_event = |
| blink::WebGestureEvent(blink::WebGestureEvent::Type::kGestureScrollUpdate, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests(), |
| blink::WebGestureDevice::kTouchpad); |
| gesture_event.SetPositionInWidget(gfx::PointF(1, 1)); |
| gesture_event.data.scroll_update.delta_x = 0.0f; |
| gesture_event.data.scroll_update.delta_y = 6.0f; |
| gesture_event.data.scroll_update.velocity_x = 0; |
| gesture_event.data.scroll_update.velocity_y = 0; |
| rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event); |
| |
| gesture_event = |
| blink::WebGestureEvent(blink::WebGestureEvent::Type::kGestureScrollEnd, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests(), |
| blink::WebGestureDevice::kTouchpad); |
| gesture_event.SetPositionInWidget(gfx::PointF(1, 1)); |
| rwhv_nested->GetRenderWidgetHost()->ForwardGestureEvent(gesture_event); |
| |
| interceptor->WaitForRect(); |
| update_rect = interceptor->last_rect(); |
| // As above, if this loop does not terminate then it indicates an issue |
| // with scroll bubbling. |
| while (update_rect.y() > initial_y) { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); |
| run_loop.Run(); |
| update_rect = interceptor->last_rect(); |
| } |
| |
| // Test that when the child frame absorbs all of the scroll delta, it does |
| // not propagate to the parent (see https://crbug.com/621624). |
| interceptor->ResetRectRunLoop(); |
| scroll_event.delta_y = -5.0f; |
| scroll_event.dispatch_type = blink::WebInputEvent::DispatchType::kBlocking; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_nested->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| // It isn't possible to busy loop waiting on the renderer here because we |
| // are explicitly testing that something does *not* happen. This creates a |
| // small chance of false positives but shouldn't result in false negatives, |
| // so flakiness implies this test is failing. |
| { |
| base::RunLoop run_loop; |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout()); |
| run_loop.Run(); |
| } |
| DCHECK_EQ(interceptor->last_rect().x(), 0); |
| DCHECK_EQ(interceptor->last_rect().y(), 0); |
| } |
| |
| // Tests that scrolling with the keyboard will bubble unused scroll to the |
| // OOPIF's parent. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, |
| KeyboardScrollBubblingFromOOPIF) { |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_iframe_in_scrollable_div.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* iframe_node = root->child_at(0); |
| |
| 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)); |
| |
| RenderWidgetHostViewBase* rwhv_child = static_cast<RenderWidgetHostViewBase*>( |
| iframe_node->current_frame_host()->GetRenderWidgetHost()->GetView()); |
| |
| // This test does not involve hit testing, but input events could be dropped |
| // by the renderer before the first compositor commit, so we wait here anyway |
| // to avoid that. |
| WaitForHitTestData(iframe_node->current_frame_host()); |
| |
| EXPECT_DOUBLE_EQ( |
| 0.0, |
| EvalJs(root, |
| "var wrapperDiv = document.getElementById('wrapper-div');" |
| "var initial_y = wrapperDiv.scrollTop;" |
| "var waitForScrollDownPromise = new Promise(function(resolve) {" |
| " wrapperDiv.addEventListener('scroll', () => {" |
| " if (wrapperDiv.scrollTop > initial_y)" |
| " resolve(wrapperDiv.scrollTop);" |
| " });" |
| "});" |
| "initial_y;") |
| .ExtractDouble()); |
| |
| NativeWebKeyboardEvent key_event( |
| blink::WebKeyboardEvent::Type::kRawKeyDown, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| key_event.windows_key_code = ui::VKEY_DOWN; |
| key_event.native_key_code = |
| ui::KeycodeConverter::DomCodeToNativeKeycode(ui::DomCode::ARROW_DOWN); |
| key_event.dom_code = static_cast<int>(ui::DomCode::ARROW_DOWN); |
| key_event.dom_key = ui::DomKey::ARROW_DOWN; |
| |
| rwhv_child->GetRenderWidgetHost()->ForwardKeyboardEvent(key_event); |
| |
| key_event.SetType(blink::WebKeyboardEvent::Type::kKeyUp); |
| rwhv_child->GetRenderWidgetHost()->ForwardKeyboardEvent(key_event); |
| |
| double scrolled_y = |
| EvalJs(root, |
| "waitForScrollDownPromise.then((scrolled_y) => {" |
| " window.domAutomationController.send(scrolled_y);" |
| "});", |
| content::EXECUTE_SCRIPT_USE_MANUAL_REPLY) |
| .ExtractDouble(); |
| EXPECT_GT(scrolled_y, 0.0); |
| } |
| |
| // Ensure that the scrollability of a local subframe in an OOPIF is considered |
| // when acknowledging GestureScrollBegin events sent to OOPIFs. |
| IN_PROC_BROWSER_TEST_P(SitePerProcessBrowserTest, ScrollLocalSubframeInOOPIF) { |
| ui::GestureConfiguration::GetInstance()->set_scroll_debounce_interval_in_ms( |
| 0); |
| |
| // This must be tall enough such that the outer iframe is not scrollable. |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/frame_tree/page_with_tall_positioned_frame.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()); |
| |
| FrameTreeNode* parent_iframe_node = root->child_at(0); |
| GURL outer_frame_url(embedded_test_server()->GetURL( |
| "baz.com", "/frame_tree/page_with_positioned_frame.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(parent_iframe_node, outer_frame_url)); |
| |
| // This must be tall enough such that the inner iframe is scrollable. |
| FrameTreeNode* nested_iframe_node = parent_iframe_node->child_at(0); |
| GURL inner_frame_url( |
| embedded_test_server()->GetURL("baz.com", "/tall_page.html")); |
| EXPECT_TRUE(NavigateToURLFromRenderer(nested_iframe_node, inner_frame_url)); |
| |
| ASSERT_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://baz.com/", |
| DepictFrameTree(root)); |
| |
| RenderWidgetHostViewBase* rwhv_child = static_cast<RenderWidgetHostViewBase*>( |
| nested_iframe_node->current_frame_host() |
| ->GetRenderWidgetHost() |
| ->GetView()); |
| |
| WaitForHitTestData(parent_iframe_node->current_frame_host()); |
| |
| // When we scroll the inner frame, we should have the GSB be consumed. |
| // The outer iframe not being scrollable should not cause the GSB to go |
| // unconsumed. |
| InputEventAckWaiter ack_observer( |
| parent_iframe_node->current_frame_host()->GetRenderWidgetHost(), |
| base::BindRepeating([](blink::mojom::InputEventResultSource, |
| blink::mojom::InputEventResultState state, |
| const blink::WebInputEvent& event) { |
| return event.GetType() == |
| blink::WebGestureEvent::Type::kGestureScrollBegin && |
| state == blink::mojom::InputEventResultState::kConsumed; |
| })); |
| |
| // Wait until renderer's compositor thread is synced. Otherwise the non fast |
| // scrollable regions won't be set when the event arrives. |
| MainThreadFrameObserver observer(rwhv_child->GetRenderWidgetHost()); |
| observer.Wait(); |
| |
| // Now scroll the inner frame downward. |
| blink::WebMouseWheelEvent scroll_event( |
| blink::WebInputEvent::Type::kMouseWheel, |
| blink::WebInputEvent::kNoModifiers, |
| blink::WebInputEvent::GetStaticTimeStampForTests()); |
| scroll_event.SetPositionInWidget(90, 110); |
| scroll_event.delta_units = ui::ScrollGranularity::kScrollByPrecisePixel; |
| scroll_event.delta_x = 0.0f; |
| scroll_event.delta_y = -50.0f; |
| scroll_event.phase = blink::WebMouseWheelEvent::kPhaseBegan; |
| rwhv_child->ProcessMouseWheelEvent(scroll_event, ui::LatencyInfo()); |
| ack_observer.Wait(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| ScrollingIntegrationTest, |
| testing::ValuesIn(RenderDocumentFeatureLevelValues())); |
| INSTANTIATE_TEST_SUITE_P(All, |
| SitePerProcessScrollAnchorTest, |
| testing::ValuesIn(RenderDocumentFeatureLevelValues())); |
| INSTANTIATE_TEST_SUITE_P(All, |
| SitePerProcessProgrammaticScrollTest, |
| testing::ValuesIn(RenderDocumentFeatureLevelValues())); |
| |
| } // namespace content |