chromium/content/browser/find_request_manager_browsertest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/342213636): Remove this and spanify to fix the errors.
#pragma allow_unsafe_buffers
#endif

#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/find_in_page_client.h"
#include "content/browser/find_request_manager.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.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_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/find_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "third_party/blink/public/mojom/page/widget.mojom-test-utils.h"
#include "url/origin.h"

#if BUILDFLAG(IS_ANDROID)
#include "ui/android/view_android.h"
#endif

namespace content {

namespace {

const int kInvalidId =;

const url::Origin& GetOriginForFrameTreeNode(FrameTreeNode* node) {}

#if BUILDFLAG(IS_ANDROID)
double GetFrameDeviceScaleFactor(const ToRenderFrameHost& adapter) {
  return EvalJs(adapter, "window.devicePixelRatio;").ExtractDouble();
}
#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace

class FindRequestManagerTestBase : public ContentBrowserTest {};

class FindRequestManagerTest : public FindRequestManagerTestBase,
                               public testing::WithParamInterface<bool> {};

INSTANTIATE_TEST_SUITE_P();

// TODO(crbug.com/40470937): These tests frequently fail on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE
#else
#define MAYBE(x)
#endif


// Tests basic find-in-page functionality (such as searching forward and
// backward) and check for correct results at each step.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, MAYBE(Basic)) {}

IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, FindInPage_Issue615291) {}

bool ExecuteScriptAndExtractRect(FrameTreeNode* frame,
                                 const std::string& script,
                                 gfx::Rect* out) {}

// Basic test that a search result is actually brought into view.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, ScrollAndZoomIntoView) {}

// Tests searching for a word character-by-character, as would typically be done
// by a user typing into the find bar.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, MAYBE(CharacterByCharacter)) {}

// TODO(crbug.com/40470937): This test frequently fails on Android.
// TODO(crbug.com/41291496): This test is flaky on Win
// TODO(crbug.com/41393143): Flaky on CrOS MSan
// Tests sending a large number of find requests subsequently.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, DISABLED_RapidFire) {}

// Tests removing a frame during a find session.
// TODO(crbug.com/40489609): Test is flaky on all platforms.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, DISABLED_RemoveFrame) {}

IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, RemoveMainFrame) {}

// Tests adding a frame during a find session.
// TODO(crbug.com/40489609): Test is flaky on all platforms.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, DISABLED_AddFrame) {}

// Tests adding an in-process hidden iframe during a find session.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest,
                       AddInprocessHiddenFrameDuringFind) {}

// Tests adding a frame during a find session where there were previously no
// matches.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, MAYBE(AddFrameAfterNoMatches)) {}

// Tests a frame navigating to a different page during a find session.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, MAYBE(NavigateFrame)) {}

// Tests Searching in a hidden frame. Matches in the hidden frame should be
// ignored.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, MAYBE(HiddenFrame)) {}

// Tests that new matches can be found in dynamically added text.
// TODO(crbug.com/330194342): Deflake and re-enable.
#if BUILDFLAG(IS_ANDROID) || \
    (BUILDFLAG(IS_LINUX) && !defined(UNDEFINED_SANITIZER))
#define MAYBE_FindNewMatches
#else
#define MAYBE_FindNewMatches
#endif
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, MAYBE_FindNewMatches) {}

// TODO(crbug.com/40470937): These tests frequently fail on Android.
// TODO(crbug.com/41352658): Flaky timeout on Win7 (dbg).
// TODO(crbug.com/41408666): Flaky on Win10.
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_WIN)
#define MAYBE_FindInPage_Issue627799
#else
#define MAYBE_FindInPage_Issue627799
#endif

IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, MAYBE_FindInPage_Issue627799) {}

IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, DetachFrameWithMatch) {}

IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, MAYBE(FindInPage_Issue644448)) {}

#if BUILDFLAG(IS_ANDROID)
// Tests empty active match rect when kWrapAround is false.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, EmptyActiveMatchRect) {
  LoadAndWait("/find_in_page.html");

  // kWrapAround is false by default.
  auto default_options = blink::mojom::FindOptions::New();
  default_options->run_synchronously_for_testing = true;
  Find("result 01", default_options.Clone());
  delegate()->WaitForFinalReply();
  EXPECT_EQ(1, delegate()->GetFindResults().number_of_matches);

  // Request the find match rects.
  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();
  const std::vector<gfx::RectF>& rects = delegate()->find_match_rects();

  // The first match should be active.
  EXPECT_EQ(rects[0], delegate()->active_match_rect());

  Find("result 00", default_options.Clone());
  delegate()->WaitForFinalReply();
  EXPECT_EQ(1, delegate()->GetFindResults().number_of_matches);

  // Request the find match rects.
  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();

  // The active match rect should be empty.
  EXPECT_EQ(gfx::RectF(), delegate()->active_match_rect());
}

class MainFrameSizeChangedWaiter : public WebContentsObserver {
 public:
  MainFrameSizeChangedWaiter(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}
  void Wait() { run_loop_.Run(); }

 private:
  void FrameSizeChanged(RenderFrameHost* render_frame_host,
                        const gfx::Size& frame_size) override {
    if (render_frame_host->IsInPrimaryMainFrame())
      run_loop_.Quit();
  }

  base::RunLoop run_loop_;
};

// Tests match rects in the iframe are updated with the size of the main frame,
// and the active match rect should be in it.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTest,
                       RectsUpdateWhenMainFrameSizeChanged) {
  LoadAndWait("/find_in_page.html");

  // Make a initial size for native view.
  const int kWidth = 1080;
  const int kHeight = 1286;
  gfx::Size size(kWidth, kHeight);
  contents()->GetNativeView()->OnSizeChanged(kWidth, kHeight);
  contents()->GetNativeView()->OnPhysicalBackingSizeChanged(size);

  // Make a FindRequest for "result".
  auto options = blink::mojom::FindOptions::New();
  options->run_synchronously_for_testing = true;
  Find("result", options->Clone());
  delegate()->WaitForFinalReply();
  EXPECT_EQ(19, delegate()->GetFindResults().number_of_matches);

  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();

  // Change the size of native view.
  const int kNewHeight = 2121;
  size = gfx::Size(kWidth, kNewHeight);
  contents()->GetNativeView()->OnSizeChanged(kWidth, kNewHeight);
  contents()->GetNativeView()->OnPhysicalBackingSizeChanged(size);

  // Wait for the size of the mainframe to change, and then the position
  // of match rects should change as expected.
  MainFrameSizeChangedWaiter(contents()).Wait();

  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();
  std::vector<gfx::RectF> new_rects = delegate()->find_match_rects();

  // The first match should be active.
  EXPECT_EQ(new_rects[0], delegate()->active_match_rect());

  // Check that all active rects (including iframe) matches with corresponding
  // match rect.
  for (int i = 1; i < 19; i++) {
    options->new_session = false;
    options->forward = true;
    Find("result", options->Clone());
    delegate()->WaitForFinalReply();

    EXPECT_EQ(19, delegate()->GetFindResults().number_of_matches);

    // Request the find match rects.
    contents()->RequestFindMatchRects(-1);
    delegate()->WaitForMatchRects();
    new_rects = delegate()->find_match_rects();

    // The active rect should be equal to the corresponding match rect.
    EXPECT_EQ(new_rects[i], delegate()->active_match_rect());
  }
}

// TODO(wjmaclean): This test, if re-enabled, may require work to make it
// OOPIF-compatible.
// Tests requesting find match rects.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, MAYBE(FindMatchRects)) {
  LoadAndWait("/find_in_page.html");
  if (test_with_oopif())
    MakeChildFrameCrossProcess();

  auto default_options = blink::mojom::FindOptions::New();
  default_options->run_synchronously_for_testing = true;
  Find("result", default_options.Clone());
  delegate()->WaitForFinalReply();
  EXPECT_EQ(19, delegate()->GetFindResults().number_of_matches);

  // Request the find match rects.
  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();
  const std::vector<gfx::RectF>& rects = delegate()->find_match_rects();

  // The first match should be active.
  EXPECT_EQ(rects[0], delegate()->active_match_rect());

  // All results after the first two should be between them in find-in-page
  // coordinates. This is because results 2 to 19 are inside an iframe located
  // between results 0 and 1. This applies to the fixed div too.
  EXPECT_LT(rects[0].y(), rects[1].y());
  for (int i = 2; i < 19; ++i) {
    EXPECT_LT(rects[0].y(), rects[i].y());
    EXPECT_GT(rects[1].y(), rects[i].y());
  }

  // Result 3 should be below results 2 and 4. This is caused by the CSS
  // transform in the containing div. If the transform doesn't work then result
  // 3 will be between results 2 and 4.
  EXPECT_GT(rects[3].y(), rects[2].y());
  EXPECT_GT(rects[3].y(), rects[4].y());

  // Results 6, 7, 8 and 9 should be one below the other in that same order. If
  // overflow:scroll is not properly handled then result 8 would be below result
  // 9 or result 7 above result 6 depending on the scroll.
  EXPECT_LT(rects[6].y(), rects[7].y());
  EXPECT_LT(rects[7].y(), rects[8].y());
  EXPECT_LT(rects[8].y(), rects[9].y());

  // Results 11, 12, 13 and 14 should be between results 10 and 15, as they are
  // inside the table.
  EXPECT_GT(rects[11].y(), rects[10].y());
  EXPECT_GT(rects[12].y(), rects[10].y());
  EXPECT_GT(rects[13].y(), rects[10].y());
  EXPECT_GT(rects[14].y(), rects[10].y());
  EXPECT_LT(rects[11].y(), rects[15].y());
  EXPECT_LT(rects[12].y(), rects[15].y());
  EXPECT_LT(rects[13].y(), rects[15].y());
  EXPECT_LT(rects[14].y(), rects[15].y());

  // Result 11 should be above results 12, 13 and 14 as it's in the table
  // header.
  EXPECT_LT(rects[11].y(), rects[12].y());
  EXPECT_LT(rects[11].y(), rects[13].y());
  EXPECT_LT(rects[11].y(), rects[14].y());

  // Result 11 should also be right of results 12, 13 and 14 because of the
  // colspan.
  EXPECT_GT(rects[11].x(), rects[12].x());
  EXPECT_GT(rects[11].x(), rects[13].x());
  EXPECT_GT(rects[11].x(), rects[14].x());

  // Result 12 should be left of results 11, 13 and 14 in the table layout.
  EXPECT_LT(rects[12].x(), rects[11].x());
  EXPECT_LT(rects[12].x(), rects[13].x());
  EXPECT_LT(rects[12].x(), rects[14].x());

  // Results 13, 12 and 14 should be one above the other in that order because
  // of the rowspan and vertical-align: middle by default.
  EXPECT_LT(rects[13].y(), rects[12].y());
  EXPECT_LT(rects[12].y(), rects[14].y());

  // Result 16 should be below result 15.
  EXPECT_GT(rects[15].y(), rects[14].y());

  // Result 18 should be normalized with respect to the position:relative div,
  // and not it's immediate containing div. Consequently, result 18 should be
  // above result 17.
  EXPECT_GT(rects[17].y(), rects[18].y());
}

namespace {

class ZoomToFindInPageRectMessageFilter
    : public blink::mojom::FrameWidgetHostInterceptorForTesting {
 public:
  ZoomToFindInPageRectMessageFilter(RenderWidgetHostImpl* rwhi)
      : impl_(rwhi->frame_widget_host_receiver_for_testing().SwapImplForTesting(
            this)),
        widget_message_seen_(false) {}

  ZoomToFindInPageRectMessageFilter(const ZoomToFindInPageRectMessageFilter&) =
      delete;
  ZoomToFindInPageRectMessageFilter& operator=(
      const ZoomToFindInPageRectMessageFilter&) = delete;

  ~ZoomToFindInPageRectMessageFilter() override {}

  blink::mojom::FrameWidgetHost* GetForwardingInterface() override {
    return impl_;
  }

  void Reset() {
    widget_rect_seen_ = gfx::Rect();
    widget_message_seen_ = false;
  }

  void WaitForWidgetHostMessage() {
    if (widget_message_seen_)
      return;

    base::RunLoop run_loop;
    quit_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

  gfx::Rect& widget_message_rect() { return widget_rect_seen_; }

 private:
  void ZoomToFindInPageRectInMainFrame(const gfx::Rect& rect_to_zoom) override {
    widget_rect_seen_ = rect_to_zoom;
    widget_message_seen_ = true;
    if (!quit_closure_.is_null())
      std::move(quit_closure_).Run();
  }

  raw_ptr<blink::mojom::FrameWidgetHost> impl_;
  gfx::Rect widget_rect_seen_;
  bool widget_message_seen_;
  base::OnceClosure quit_closure_;
};

}  // namespace

// Tests activating the find match nearest to a given point.
// TODO(crbug.com/40864045): Fix flaky failures.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest,
                       DISABLED_ActivateNearestFindMatch) {
  LoadAndWait("/find_in_page.html");
  if (test_with_oopif())
    MakeChildFrameCrossProcess();

  std::unique_ptr<ZoomToFindInPageRectMessageFilter> message_interceptor_child;

  if (test_with_oopif()) {
    message_interceptor_child =
        std::make_unique<ZoomToFindInPageRectMessageFilter>(
            first_child()->current_frame_host()->GetRenderWidgetHost());
  }

  auto default_options = blink::mojom::FindOptions::New();
  default_options->run_synchronously_for_testing = true;
  Find("result", default_options.Clone());
  delegate()->WaitForFinalReply();
  EXPECT_EQ(19, delegate()->GetFindResults().number_of_matches);

  auto* find_request_manager = contents()->GetFindRequestManagerForTesting();

  // Get the find match rects.
  contents()->RequestFindMatchRects(-1);
  delegate()->WaitForMatchRects();
  const std::vector<gfx::RectF>& rects = delegate()->find_match_rects();

  double device_scale_factor = GetFrameDeviceScaleFactor(contents());

  // Activate matches via points inside each of the find match rects, in an
  // arbitrary order. Check that the correct match becomes active after each
  // activation.
  int order[19] =
      {11, 13, 2, 0, 16, 5, 7, 10, 6, 1, 15, 14, 9, 17, 18, 3, 8, 12, 4};
  for (int i = 0; i < 19; ++i) {
    delegate()->MarkNextReply();
    contents()->ActivateNearestFindResult(
        rects[order[i]].CenterPoint().x(), rects[order[i]].CenterPoint().y());
    delegate()->WaitForNextReply();

    bool is_match_in_oopif = order[i] > 1 && test_with_oopif();
    // Check widget message rect to make sure it matches.
    if (is_match_in_oopif) {
      message_interceptor_child->WaitForWidgetHostMessage();
      auto expected_rect = gfx::ScaleToEnclosingRect(
          message_interceptor_child->widget_message_rect(),
          1.f / device_scale_factor);
      EXPECT_EQ(find_request_manager->GetSelectionRectForTesting(),
                expected_rect);
      message_interceptor_child->Reset();
    }

    EXPECT_EQ(order[i] + 1, delegate()->GetFindResults().active_match_ordinal);
  }
}
#endif  // BUILDFLAG(IS_ANDROID)

// Test basic find-in-page functionality after going back and forth to the same
// page. In particular, find-in-page should continue to work after going back to
// a page using the back-forward cache.
// Flaky everywhere: https://crbug.com/1115102
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, DISABLED_HistoryBackAndForth) {}

class FindInPageDisabledForOriginBrowserClient
    : public ContentBrowserTestContentBrowserClient {};

// Tests that find-in-page won't show results for origins that disabled
// find-in-page.
IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, FindInPageDisabledForOrigin) {}

class FindTestWebContentsPrerenderingDelegate
    : public FindTestWebContentsDelegate {};

class FindRequestManagerPrerenderingTest : public FindRequestManagerTest {};

// Tests that find-in-page won't show results inside a prerendering page.
IN_PROC_BROWSER_TEST_F(FindRequestManagerPrerenderingTest, Basic) {}

class FindRequestManagerTestWithBFCache : public FindRequestManagerTest {};

// Test basic find-in-page functionality when a page gets into and out of
// BFCache.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTestWithBFCache, Basic) {}

class WaitForFindTestWebContentsDelegate : public FindTestWebContentsDelegate {};

class FindRequestManagerFencedFrameTest : public FindRequestManagerTest {};

// This find-in-page client will make the find-request-queue not empty so that
// we can test a fenced frame doesn't clear the find-request-queue when it's
// deleted. To keep the find-request-queue not empty, this class
// intercepts the Mojo methods calls, and changes the FindMatchUpdateType to
// kMoreUpdatesComing (including those that were marked as kFinalUpdate), so
// that the find-request-queue won't get popped and will stay non-empty.
class NeverFinishFencedFrameFindInPageClient : public FindInPageClient {};

static std::unique_ptr<FindInPageClient> CreateFencedFrameFindInPageClient(
    FindRequestManager* find_request_manager,
    RenderFrameHostImpl* rfh) {}

// Tests that a main frame, a sub frame, and a fenced frame clear the
// find-request-queue when the fenced frame is deleted.
IN_PROC_BROWSER_TEST_F(FindRequestManagerFencedFrameTest,
                       OnlyPrimaryMainFrameClearsFindRequestQueue) {}

// This find-in-page client will make it so that we never stop listening for
// find-in-page updates only for subframes, through modifying final updates to
// be marked as non-final updates. It helps us to simulate various things that
// can happen before a find-in-page session finishes (e.g. navigation,
// lifecycle state change) without finishing the find session.
class NeverFinishSubframeFindInPageClient : public FindInPageClient {};

class FindRequestManagerTestObserver : public WebContentsObserver {};

static std::unique_ptr<FindInPageClient> CreateFindInPageClient(
    FindRequestManager* find_request_manager,
    RenderFrameHostImpl* rfh) {}

enum class FrameSiteType {};

enum class FrameTestType {};

class FindRequestManagerTestWithTestConfig
    : public FindRequestManagerTestBase,
      public testing::WithParamInterface<
          ::testing::tuple<FrameSiteType, FrameTestType>> {};

INSTANTIATE_TEST_SUITE_P();

// Tests that the previous results from old document are removed and we get the
// new results from the new document when we navigate the subframe that
// hasn't finished the find-in-page session to the new document.
// TODO(crbug.com/40220234): Fix flakiness and reenable the test.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || \
    BUILDFLAG(IS_ANDROID)
#define MAYBE_NavigateFrameDuringFind
#else
#define MAYBE_NavigateFrameDuringFind
#endif
IN_PROC_BROWSER_TEST_P(FindRequestManagerTestWithTestConfig,
                       MAYBE_NavigateFrameDuringFind) {}

// Tests that the previous results from the old documents are removed and we
// get the new results from the new document when we go back to the page in
// BFCache from the page that hasn't finished the find-in-page session.
// This TC does not intentionally check the |active_match_ordinal| value,
// because the main frame is not focused on Android, so it has a different
// result on Android.
IN_PROC_BROWSER_TEST_F(FindRequestManagerTestWithBFCache,
                       NavigateFrameDuringFind) {}

IN_PROC_BROWSER_TEST_F(FindRequestManagerTest, CrashDuringFind) {}

IN_PROC_BROWSER_TEST_P(FindRequestManagerTest, DelayThenStop) {}

}  // namespace content