chromium/ios/web/find_in_page/java_script_find_in_page_manager_impl_unittest.mm

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

#import "ios/web/find_in_page/java_script_find_in_page_manager_impl.h"

#import "base/memory/raw_ptr.h"
#import "base/run_loop.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/user_action_tester.h"
#import "base/values.h"
#import "ios/web/find_in_page/find_in_page_constants.h"
#import "ios/web/find_in_page/find_in_page_java_script_feature.h"
#import "ios/web/js_messaging/java_script_feature_manager.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_find_in_page_manager_delegate.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_test.h"
#import "testing/gtest/include/gtest/gtest.h"

using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

namespace web {

// Tests JavaScriptFindInPageManagerImpl and verifies that the state of
// FindInPageManagerDelegate is correct depending on what web frames return.
class JavaScriptFindInPageManagerImplTest : public WebTest {
 protected:
  JavaScriptFindInPageManagerImplTest()
      : WebTest(std::make_unique<FakeWebClient>()) {}

  void SetUp() override {
    WebTest::SetUp();

    FindInPageJavaScriptFeature* feature =
        FindInPageJavaScriptFeature::GetInstance();

    fake_web_state_ = std::make_unique<FakeWebState>();
    fake_web_state_->SetBrowserState(GetBrowserState());
    auto frames_manager = std::make_unique<FakeWebFramesManager>();
    fake_web_frames_manager_ = frames_manager.get();
    fake_web_state_->SetWebFramesManager(feature->GetSupportedContentWorld(),
                                         std::move(frames_manager));

    JavaScriptFeatureManager::FromBrowserState(GetBrowserState())
        ->ConfigureFeatures({feature});
    JavaScriptFindInPageManager::CreateForWebState(fake_web_state_.get());
    GetFindInPageManager()->SetDelegate(&fake_delegate_);
  }

  // Returns the JavaScriptFindInPageManager associated with `fake_web_state_`.
  JavaScriptFindInPageManager* GetFindInPageManager() {
    return JavaScriptFindInPageManager::FromWebState(fake_web_state_.get());
  }

  // Returns a fake WebFrame that represents the main frame which will return
  // `js_result` for the JavaScript function call "findInString.findString".
  std::unique_ptr<FakeWebFrame> CreateMainWebFrameWithJsResultForFind(
      base::Value* js_result) {
    auto frame = FakeWebFrame::CreateMainWebFrame(GURL());
    frame->AddJsResultForFunctionCall(js_result, kFindInPageSearch);
    frame->set_browser_state(GetBrowserState());
    return frame;
  }

  // Returns a fake WebFrame that represents a child frame which will return
  // `js_result` for the JavaScript function call "findInString.findString".
  std::unique_ptr<FakeWebFrame> CreateChildWebFrameWithJsResultForFind(
      base::Value* js_result) {
    auto frame = FakeWebFrame::CreateChildWebFrame(GURL());
    frame->AddJsResultForFunctionCall(js_result, kFindInPageSearch);
    frame->set_browser_state(GetBrowserState());
    return frame;
  }

  void AddWebFrame(std::unique_ptr<FakeWebFrame> frame) {
    fake_web_frames_manager_->AddWebFrame(std::move(frame));
  }

  void RemoveWebFrame(const std::string& frame_id) {
    fake_web_frames_manager_->RemoveWebFrame(frame_id);
  }

  std::unique_ptr<FakeWebState> fake_web_state_;
  raw_ptr<FakeWebFramesManager> fake_web_frames_manager_;
  FakeFindInPageManagerDelegate fake_delegate_;
  base::UserActionTester user_action_tester_;
};

// Tests that Find In Page responds with a total match count of three when a
// frame has one match and another frame has two matches.
TEST_F(JavaScriptFindInPageManagerImplTest, FindMatchesMultipleFrames) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(2ul, frame_with_one_match_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());
  EXPECT_EQ(3, fake_delegate_.state()->match_count);
}

// Tests that Find In Page responds with a total match count of one when a frame
// has one match but find in one frame was cancelled. This can occur if the
// frame becomes unavailable.
TEST_F(JavaScriptFindInPageManagerImplTest, FrameCancelFind) {
  auto null = std::make_unique<base::Value>();
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_null_result =
      CreateMainWebFrameWithJsResultForFind(null.get());
  FakeWebFrame* frame_with_null_result_ptr = frame_with_null_result.get();
  auto frame_with_one_match = CreateChildWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  AddWebFrame(std::move(frame_with_null_result));
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_null_result_ptr->GetLastJavaScriptCall());
  ASSERT_EQ(2ul, frame_with_one_match_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(1, fake_delegate_.state()->match_count);
}

// Tests that Find In Page returns a total match count matching the latest find
// if two finds are called.
TEST_F(JavaScriptFindInPageManagerImplTest, ReturnLatestFind) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  fake_delegate_.Reset();

  RemoveWebFrame(kMainFakeFrameId);
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(3ul, frame_with_two_matches_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[2]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(2, fake_delegate_.state()->match_count);
}

// Tests that Find In Page should not return if the web state is destroyed
// during a find.
TEST_F(JavaScriptFindInPageManagerImplTest, DestroyWebStateDuringFind) {
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  fake_web_state_.reset();
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(fake_delegate_.state());
}

// Tests that Find In Page updates total match count when a frame with matches
// becomes unavailable during find.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FrameUnavailableAfterDelegateCallback) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  fake_delegate_.Reset();

  RemoveWebFrame(kChildFakeFrameId);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));

  ASSERT_EQ(2ul, frame_with_one_match_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(1, fake_delegate_.state()->match_count);
}

// Tests that Find In Page returns with the right match count for a frame with
// one match and another that requires pumping to return its two matches.
TEST_F(JavaScriptFindInPageManagerImplTest, FrameRespondsWithPending) {
  auto negative_one = std::make_unique<base::Value>(-1.0);
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);

  std::unique_ptr<FakeWebFrame> frame_with_two_matches =
      CreateMainWebFrameWithJsResultForFind(negative_one.get());
  frame_with_two_matches->AddJsResultForFunctionCall(two.get(),
                                                     kFindInPagePump);
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_two_matches));
  auto frame_with_one_match = CreateChildWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(3ul, frame_with_two_matches_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[2]);
  EXPECT_EQ(u"__gCrWeb.findInPage.pumpSearch(100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());
  EXPECT_EQ(3, fake_delegate_.state()->match_count);
}

// Tests that Find In Page doesn't fail when delegate is not set.
TEST_F(JavaScriptFindInPageManagerImplTest, DelegateNotSet) {
  GetFindInPageManager()->SetDelegate(nullptr);
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());
  base::RunLoop().RunUntilIdle();
}

// Tests that  Find In Page responds with a total match count of zero when there
// are no known webpage frames.
TEST_F(JavaScriptFindInPageManagerImplTest, NoFrames) {
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(0, fake_delegate_.state()->match_count);
}

// Tests that Find in Page responds with a total match count of zero when there
// are no matches in the only frame. Tests that Find in Page also did not
// respond with an selected match index value.
TEST_F(JavaScriptFindInPageManagerImplTest, FrameWithNoMatchNoHighlight) {
  auto zero = std::make_unique<base::Value>(0.0);
  auto frame_with_zero_matches =
      CreateMainWebFrameWithJsResultForFind(zero.get());
  FakeWebFrame* frame_with_zero_matches_ptr = frame_with_zero_matches.get();
  AddWebFrame(std::move(frame_with_zero_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(1ul,
            frame_with_zero_matches_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_zero_matches_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(0, fake_delegate_.state()->match_count);
  EXPECT_EQ(-1, fake_delegate_.state()->index);
}

// Tests that Find in Page responds with index zero after a find when there are
// two matches in a frame.
TEST_F(JavaScriptFindInPageManagerImplTest, DidHighlightFirstIndex) {
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_two_matches =
      CreateMainWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(2ul, frame_with_two_matches_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[0]);
  EXPECT_EQ(0, fake_delegate_.state()->index);
}

// Tests that Find in Page responds with index one to a FindInPageNext find
// after a FindInPageSearch find finishes when there are two matches in a frame.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidHighlightSecondIndexAfterNextCall) {
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_two_matches =
      CreateMainWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(2ul, frame_with_two_matches_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetJavaScriptCallHistory()[0]);

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state()->index > -1;
  }));
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(1);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());
  EXPECT_EQ(1, fake_delegate_.state()->index);
}

// Tests that Find in Page selects all matches in a page with one frame with one
// match and another with two matches when making successive FindInPageNext
// calls.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidSelectAllMatchesWithNextCall) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(0, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(1, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(2, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(1);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(0, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());
}

// Tests that Find in Page selects all matches in a page with one frame with one
// match and another with two matches when making successive FindInPagePrevious
// calls.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidLoopThroughAllMatchesWithPreviousCall) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(0, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(2, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(1);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(1, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(0, fake_delegate_.state()->index);
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetLastJavaScriptCall());
}

// Tests that Find in Page responds with index two to a FindInPagePrevious find
// after a FindInPageSearch find finishes when there are two matches in a
// frame and one match in another.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidHighlightLastIndexAfterPreviousCall) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());
  ASSERT_EQ(2ul, frame_with_one_match_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[1]);
  EXPECT_EQ(u"__gCrWeb.findInPage.findString(\"foo\", 100.0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[0]);

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state()->index == 2;
  }));
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(1);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());
}

// Tests that Find in Page does not respond to a FindInPageNext or a
// FindInPagePrevious call if no FindInPageSearch find was executed beforehand.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidNotRepondToNextOrPrevIfNoSearch) {
  auto three = std::make_unique<base::Value>(3.0);
  auto frame_with_three_matches =
      CreateMainWebFrameWithJsResultForFind(three.get());
  AddWebFrame(std::move(frame_with_three_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(fake_delegate_.state());

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(fake_delegate_.state());
}

// Tests that Find in Page responds with index one for a successive
// FindInPageNext after the frame containing the currently selected match is
// removed.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidHighlightNextMatchAfterFrameDisappears) {
  auto one = std::make_unique<base::Value>(1.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  FakeWebFrame* frame_with_one_match_ptr = frame_with_one_match.get();
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_two_matches_ptr = frame_with_two_matches.get();
  AddWebFrame(std::move(frame_with_one_match));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));
  ASSERT_EQ(2ul, frame_with_one_match_ptr->GetJavaScriptCallHistory().size());
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_one_match_ptr->GetJavaScriptCallHistory()[1]);

  RemoveWebFrame(kMainFakeFrameId);
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state()->index == 0;
  }));
  EXPECT_EQ(u"__gCrWeb.findInPage.selectAndScrollToVisibleMatch(0);",
            frame_with_two_matches_ptr->GetLastJavaScriptCall());
}

// Tests that Find in Page does not respond when frame is removed
TEST_F(JavaScriptFindInPageManagerImplTest, FindDidNotRepondAfterFrameRemoved) {
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  AddWebFrame(std::move(frame_with_one_match));

  RemoveWebFrame(kMainFakeFrameId);
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(fake_delegate_.state());
}

// Tests that Find in Page responds with a total match count of one to a
// FindInPageSearch find when there is one match in a frame and then responds
// with a total match count of zero when that frame is removed.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindInPageUpdateMatchCountAfterFrameRemoved) {
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));

  RemoveWebFrame(kMainFakeFrameId);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state()->match_count == 0;
  }));
}

// Tests that DidHighlightMatches is not called when a frame with no matches is
// removed from the page.
TEST_F(JavaScriptFindInPageManagerImplTest,
       FindDidNotResponseAfterFrameDisappears) {
  auto zero = std::make_unique<base::Value>(0.0);
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_zero_matches =
      CreateMainWebFrameWithJsResultForFind(zero.get());
  auto frame_with_two_matches =
      CreateChildWebFrameWithJsResultForFind(two.get());
  AddWebFrame(std::move(frame_with_zero_matches));
  AddWebFrame(std::move(frame_with_two_matches));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state();
  }));

  fake_delegate_.Reset();
  RemoveWebFrame(kMainFakeFrameId);

  EXPECT_FALSE(fake_delegate_.state());
}

// Tests that Find in Page SetContentIsHTML() returns true if the web state's
// content is HTML and returns false if the web state's content is not HTML.
TEST_F(JavaScriptFindInPageManagerImplTest, FindInPageCanSearchContent) {
  fake_web_state_->SetContentIsHTML(false);

  EXPECT_FALSE(GetFindInPageManager()->CanSearchContent());

  fake_web_state_->SetContentIsHTML(true);

  EXPECT_TRUE(GetFindInPageManager()->CanSearchContent());
}

// Tests that Find in Page resets the match count to 0 and the query to nil
// after calling StopFinding().
TEST_F(JavaScriptFindInPageManagerImplTest, FindInPageCanStopFind) {
  auto one = std::make_unique<base::Value>(1.0);
  auto frame_with_one_match = CreateMainWebFrameWithJsResultForFind(one.get());
  AddWebFrame(std::move(frame_with_one_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state() && fake_delegate_.state()->match_count == 1;
  }));

  GetFindInPageManager()->StopFinding();
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state() && fake_delegate_.state()->match_count == 0;
  }));
  EXPECT_FALSE(fake_delegate_.state()->query);
}

// Tests that Find in Page responds with an updated match count when calling
// FindInPageNext after the visible match count in a frame changes following a
// FindInPageSearch. This simulates a once hidden match becoming visible between
// a FindInPageSearch and a FindInPageNext.
TEST_F(JavaScriptFindInPageManagerImplTest, FindInPageNextUpdatesMatchCount) {
  auto two = std::make_unique<base::Value>(2.0);
  auto frame_with_hidden_match =
      CreateMainWebFrameWithJsResultForFind(two.get());
  FakeWebFrame* frame_with_hidden_match_ptr = frame_with_hidden_match.get();
  AddWebFrame(std::move(frame_with_hidden_match));

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state() && fake_delegate_.state()->match_count == 2;
  }));
  base::Value::Dict select_and_scroll_result;
  select_and_scroll_result.Set("matches", 3.0);
  select_and_scroll_result.Set("index", 1.0);
  base::Value select_and_scroll_result_value(
      std::move(select_and_scroll_result));
  frame_with_hidden_match_ptr->AddJsResultForFunctionCall(
      &select_and_scroll_result_value, kFindInPageSelectAndScrollToMatch);

  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state() && fake_delegate_.state()->match_count == 3;
  }));
  EXPECT_EQ(1, fake_delegate_.state()->index);
}

// Tests that Find in Page logs correct UserActions for given API calls.
TEST_F(JavaScriptFindInPageManagerImplTest, FindUserActions) {
  // Setup fake page with three matches.
  auto three = std::make_unique<base::Value>(3.0);
  auto frame_with_three_matches =
      CreateMainWebFrameWithJsResultForFind(three.get());
  AddWebFrame(std::move(frame_with_three_matches));

  ASSERT_EQ(0,
            user_action_tester_.GetActionCount("IOS.FindInPage.SearchStarted"));
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch);
  EXPECT_EQ(1,
            user_action_tester_.GetActionCount("IOS.FindInPage.SearchStarted"));

  // Wait for JavaScript completion. This is required as the FindNext
  // and FindPrevious user actions are only recorded if a sufficient number of
  // matches has been reported to the manager (>2).
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    base::RunLoop().RunUntilIdle();
    return fake_delegate_.state() && fake_delegate_.state()->match_count == 3;
  }));

  ASSERT_EQ(0, user_action_tester_.GetActionCount("IOS.FindInPage.FindNext"));
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext);
  EXPECT_EQ(1, user_action_tester_.GetActionCount("IOS.FindInPage.FindNext"));

  ASSERT_EQ(0,
            user_action_tester_.GetActionCount("IOS.FindInPage.FindPrevious"));
  GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious);
  EXPECT_EQ(1,
            user_action_tester_.GetActionCount("IOS.FindInPage.FindPrevious"));
}

}  // namespace web