chromium/content/browser/renderer_host/scroll_into_view_browsertest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <string>

#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
#include "third_party/blink/public/mojom/scroll/scroll_into_view_params.mojom.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/events/event_constants.h"
#include "url/gurl.h"

#if defined(USE_AURA)
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#endif

#define EXPECT_TRUE_OR_FAIL(condition)

namespace content {

namespace {

// Test variants

// kLocalFrame will force all remote frames in a test to be local.
enum TestFrameType {};

// Tests run with both Left-to-Right and Right-to-Left writing modes.
enum TestWritingMode {};

// What kind of scroll into view to invoke, via JavaScript binding
// (element.scrollIntoView), using the InputHandler
// ScrollFocusedEditableNodeIntoView method, or via setting an OSK inset.
enum TestInvokeMethod {};

[[maybe_unused]] std::string DescribeFrameType(
    const testing::TestParamInfo<TestFrameType>& info) {}

blink::mojom::FrameWidgetInputHandler* GetInputHandler(FrameTreeNode* node) {}

// Will block from the destructor until a ScrollFocusedEditableNodeIntoView has
// completed. This must be called with the root frame tree node since that's
// where the ScrollIntoView and PageScaleAnimation will bubble to.
class ScopedFocusScrollWaiter {};

// While this is in scope, causes the TextInputManager of the given WebContents
// to always return nullptr. This effectively blocks the IME from receiving any
// events from the renderer. Note: RenderWidgetHostViewBase caches this value
// so for this to work it must be constructed before the target page is
// constructed.
class ScopedSuppressImeEvents {};

// Interceptor that can be used to verify calls to
// ScrollRectToVisibleInParentFrame on the LocalFrameHost interface.
class ScrollRectToVisibleInParentFrameInterceptor
    : public blink::mojom::LocalFrameHostInterceptorForTesting {};

// Test harness for ScrollIntoView related browser tests. These tests are
// mainly concerned with behavior of scroll into view related functionality
// across remote frames. This harness depends on
// cross_site_scroll_into_view_factory.html, which is based on
// cross_site_iframe_factory.html.
//
// cross_site_scroll_into_view_factory.html builds a frame tree from its given
// argument, allowing only a single child frame in each frame. The inner most
// frame adds an <input> element which can be used to call
// ScrollFocusedEditableNodeIntoView.
//
// Each test starts by performing a non-scrolling focus on the <input> element.
// It then performs a scroll into view (either via JavaScript bindings or
// content API) and ensures the caret is within a vertically centered band of
// the viewport.
class ScrollIntoViewBrowserTestBase : public ContentBrowserTest {};

// Runs tests in all combinations of Local/Remote frames,
// left-to-right/right-to-left writing modes, and scrollIntoView via
// element.scrollIntoView/InputHandler.ScrollFocusedEditableNodeIntoView. The
// kAuraOnScreenKeyboard is intentionally omitted as it is expected to be
// functionally equivalent to kInputHandler.
class ScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<
          std::tuple<TestFrameType, TestWritingMode, TestInvokeMethod>> {};

// See comment in SetupTest for frame tree syntax.

// ScrollIntoViewBrowserTest runs with all combinations of multiple parameters
// to test the basic scroll into view machinery so each test instantiates 8
// cases. To avoid an explosion of tests, prefer to add new tests to a more
// specific suite unless the functionality it's testing is likely to differ
// across the various parameters and isn't already covered.

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInSingleNestedFrame) {}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInLocalRoot) {}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInDoublyNestedFrame) {}

IN_PROC_BROWSER_TEST_P(
    ScrollIntoViewBrowserTest,
    CrossesEditableInDoublyNestedFrameLocalAndRemoteBoundaries) {}

INSTANTIATE_TEST_SUITE_P();

#if defined(USE_AURA)

// Tests viewport insetting as a result of keyboard insets. Insetting is only
// used on Aura platforms. The OSK on Android resizes the entire view.
class InsetScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<TestFrameType> {};

// Ensure that insetting the viewport causes the visual viewport to be resized
// and focused editable scrolled into view. (https://crbug.com/927483)
IN_PROC_BROWSER_TEST_P(InsetScrollIntoViewBrowserTest,
                       InsetsCauseScrollToFocusedEditable) {}

INSTANTIATE_TEST_SUITE_P();

#endif

// Only Chrome Android performs a zoom when focusing an editable.
#if BUILDFLAG(IS_ANDROID)

constexpr double kMobileMinimumScale = 0.25;

// Tests zooming behaviors for ScrollFocusedEditableNodeIntoView. These tests
// runs only on Android since that's the only platorm that uses this behavior.
class ZoomScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<TestFrameType> {
 public:
  bool IsForceLocalFrames() const override { return GetParam() == kLocalFrame; }

  bool IsWritingModeLTR() const override { return true; }

  TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};

// A regular "desktop" site (i.e. no viewport <meta> tag) on Chrome Android
// should zoom in on a focused editable so that it's legible.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest, DesktopViewportMustZoom) {
  ASSERT_TRUE(SetupTest("siteA(siteB)"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // Without a viewport tag, the page is considered a "desktop" page so we
  // should enable zooming to a legible scale.
  EXPECT_NEAR(1, GetVisualViewport().scale, 0.05);
}

// Ensure that adding a `width=device-width` viewport <meta> tag disables the
// zooming behavior so that "mobile-friendly" pages do not zoom in on input
// boxes.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
                       MobileViewportDisablesZoom) {
  ASSERT_TRUE(SetupTest("siteA{MobileViewport}(siteB)"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // width=device-width must prevent the zooming behavior.
  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}

// Similar to above, an input in a touch-action region that disables pinch-zoom
// shouldn't cause zoom since it may trap the user at that zoom level.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
                       TouchActionNoneDisablesZoom) {
  ASSERT_TRUE(SetupTest("siteA(siteB{TouchActionNone})"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // touch-action: none must prevent the zooming behavior since the user may
  // not be able to zoom back out.
  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}

class RootScrollerScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase {
 public:
  bool IsForceLocalFrames() const override { return false; }
  bool IsWritingModeLTR() const override { return true; }
  TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};

IN_PROC_BROWSER_TEST_F(RootScrollerScrollIntoViewBrowserTest,
                       FocusInRootScroller) {
  ASSERT_TRUE(SetupTest("siteA{RootScroller,MobileViewportNoZoom}"));

  // Root scroller is recomputed after a Blink lifecycle so ensure a frame is
  // produced to make sure the renderer has had time to evaluate the root
  // scroller.
  {
    base::RunLoop loop;
    shell()->web_contents()->GetPrimaryMainFrame()->InsertVisualStateCallback(
        base::BindLambdaForTesting(
            [&loop](bool visual_state_updated) { loop.Quit(); }));
    loop.Run();
  }

  ASSERT_EQ(1.0, GetVisualViewport().scale);
  ASSERT_EQ(
      true,
      EvalJs(
          InnerMostFrameTreeNode(),
          "window.internals.effectiveRootScroller(document).tagName == 'DIV'"));

  RunTest();
}

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         ZoomScrollIntoViewBrowserTest,
                         testing::Values(kLocalFrame, kRemoteFrame),
                         DescribeFrameType);
#endif

// Tests scrollIntoView behaviors related to a fenced frame.
class ScrollIntoViewFencedFrameBrowserTest
    : public ScrollIntoViewBrowserTestBase {};

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       SingleFencedFrame) {}

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       NestedFencedFrames) {}

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       LocalFrameInFencedFrame) {}

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       RemoteFrameInFencedFrame) {}

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       FencedFrameInRemoteFrame) {}

IN_PROC_BROWSER_TEST_F(ScrollIntoViewFencedFrameBrowserTest,
                       ProgrammaticScrollIntoViewDoesntCrossFencedFrame) {}

}  // namespace

}  // namespace content