| // Copyright 2023 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/find_in_page_manager_impl.h" |
| |
| #import "base/test/ios/wait_util.h" |
| #import "base/test/metrics/user_action_tester.h" |
| #import "ios/web/public/test/fakes/crw_fake_find_interaction.h" |
| #import "ios/web/public/test/fakes/crw_fake_find_session.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_state.h" |
| #import "ios/web/public/test/web_test.h" |
| #import "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| // Timeout before failure if the FindInPageManager does not report to the |
| // delegate fast enough. |
| const base::TimeDelta kWaitForFindCompletionTimeout = base::Milliseconds(100); |
| |
| } // namespace |
| |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| |
| namespace web { |
| |
| // Tests FindInPageManagerImpl and verifies that the state of |
| // FindInPageManagerDelegate is correct depending on what the Find session |
| // returns. FindInPageManagerImplTest is part of an iOS 16+ feature so this |
| // test class only applies to iOS 16+. For the old Find in Page tests, please |
| // see java_script_find_in_page_manager_impl_unittest.mm |
| class FindInPageManagerImplTest : public WebTest { |
| protected: |
| FindInPageManagerImplTest() : WebTest(std::make_unique<FakeWebClient>()) {} |
| |
| void SetUp() override { |
| WebTest::SetUp(); |
| |
| // Skip setup if this is <iOS 16. |
| if (@available(iOS 16, *)) { |
| fake_web_state_ = std::make_unique<FakeWebState>(); |
| fake_web_state_->SetBrowserState(GetBrowserState()); |
| |
| FindInPageManagerImpl::CreateForWebState(fake_web_state_.get()); |
| GetFindInPageManager()->SetDelegate(&fake_delegate_); |
| // Sets a smaller delay between each manager's call to |
| // `PollActiveFindSession()` so tests run faster. |
| GetFindInPageManager()->poll_active_find_session_delay_ = |
| base::Milliseconds(5); |
| |
| // Enable and set up fake Find interaction in the fake web state. |
| fake_web_state_->SetFindInteractionEnabled(true); |
| fake_web_state_->SetFindInteraction( |
| [[CRWFakeFindInteraction alloc] init]); |
| } |
| } |
| |
| // Sets the fake Find session analyzed by the FindInPageManager. |
| void SetFindSession(CRWFakeFindSession* find_session) API_AVAILABLE(ios(16)) { |
| static_cast<CRWFakeFindInteraction*>(fake_web_state_->GetFindInteraction()) |
| .activeFindSession = find_session; |
| } |
| |
| // Create a fake Find session which returns the appropriate match counts for |
| // given queries in `result_counts_for_queries`. |
| CRWFakeFindSession* CreateFindSessionWithResultCountsForQueries( |
| ResultCountsForQueries* result_counts_for_queries) |
| API_AVAILABLE(ios(16)) { |
| CRWFakeFindSession* find_session = [[CRWFakeFindSession alloc] init]; |
| find_session.resultCountsForQueries = result_counts_for_queries; |
| return find_session; |
| } |
| |
| // Sets a fake Find session with given fake result counts for given queries in |
| // `result_counts_for_queries`, |
| // to be analyzed by the FindInPageManager. |
| void SetFindSessionWithResultCountsForQueries( |
| ResultCountsForQueries* result_counts_for_queries) |
| API_AVAILABLE(ios(16)) { |
| SetFindSession( |
| CreateFindSessionWithResultCountsForQueries(result_counts_for_queries)); |
| } |
| |
| // Wait until the delegate state has been set or time is out. |
| bool WaitForStateOrTimeout() API_AVAILABLE(ios(16)) { |
| return WaitUntilConditionOrTimeout(kWaitForFindCompletionTimeout, ^bool { |
| base::RunLoop().RunUntilIdle(); |
| return fake_delegate_.state(); |
| }); |
| } |
| |
| // Wait until the index verifies `index_predicate` in the delegate state. |
| bool WaitForIndexOrTimeout(bool (^index_predicate)(int)) |
| API_AVAILABLE(ios(16)) { |
| return WaitUntilConditionOrTimeout(kWaitForFindCompletionTimeout, ^bool { |
| base::RunLoop().RunUntilIdle(); |
| return fake_delegate_.state() && |
| index_predicate(fake_delegate_.state()->index); |
| }); |
| } |
| |
| // Wait until the index is different from -1 in the delegate state. |
| bool WaitForValidIndexOrTimeout() API_AVAILABLE(ios(16)) { |
| return WaitForIndexOrTimeout(^(int index) { |
| return index != -1; |
| }); |
| } |
| |
| // Returns the FindInPageManager associated with `fake_web_state_`. |
| FindInPageManagerImpl* GetFindInPageManager() API_AVAILABLE(ios(16)) { |
| return static_cast<FindInPageManagerImpl*>( |
| FindInPageManager::FromWebState(fake_web_state_.get())); |
| } |
| |
| // Get the active Find session in the FindInPageManager. |
| id<CRWFindSession> GetActiveFindSession() API_AVAILABLE(ios(16)) { |
| return GetFindInPageManager()->GetActiveFindSession(); |
| } |
| |
| std::unique_ptr<FakeWebState> fake_web_state_ API_AVAILABLE(ios(16)); |
| FakeFindInPageManagerDelegate fake_delegate_ API_AVAILABLE(ios(16)); |
| base::UserActionTester user_action_tester_ API_AVAILABLE(ios(16)); |
| }; |
| |
| // Tests that Find In Page responds with a total match count of three when it |
| // calls Find on a query with three matches. |
| TEST_F(FindInPageManagerImplTest, FindThreeMatches) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_EQ(3, 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(FindInPageManagerImplTest, ReturnLatestFind) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3, @"bar" : @2}); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| fake_delegate_.Reset(); |
| |
| GetFindInPageManager()->Find(@"bar", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_EQ(2, fake_delegate_.state()->match_count); |
| } |
| } |
| |
| // Tests that the Find In Page manager does not report to the delegate if the |
| // web state is destroyed during a Find operation. |
| TEST_F(FindInPageManagerImplTest, DestroyWebStateDuringFind) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| fake_web_state_.reset(); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(fake_delegate_.state()); |
| } |
| } |
| |
| // Tests that Find In Page doesn't fail when delegate is not set. |
| TEST_F(FindInPageManagerImplTest, DelegateNotSet) { |
| if (@available(iOS 16, *)) { |
| GetFindInPageManager()->SetDelegate(nullptr); |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForFindCompletionTimeout, ^{ |
| base::RunLoop().RunUntilIdle(); |
| return GetActiveFindSession().resultCount == 3; |
| })); |
| } |
| } |
| |
| // Tests that Find in Page manager responds with a total match count of zero |
| // when there are no matches in the web page. Tests that Find in Page also did |
| // not respond with a valid selected match index value. |
| TEST_F(FindInPageManagerImplTest, PageWithNoMatchNoHighlight) { |
| if (@available(iOS 16, *)) { |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| 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 |
| // three matches in a page. |
| TEST_F(FindInPageManagerImplTest, DidHighlightFirstIndex) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| 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 page. |
| TEST_F(FindInPageManagerImplTest, FindDidHighlightSecondIndexAfterNextCall) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @2}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext); |
| |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(1, fake_delegate_.state()->index); |
| } |
| } |
| |
| // Tests that Find in Page selects all matches in a page with three matches and |
| // wraps when making successive FindInPageNext calls. |
| TEST_F(FindInPageManagerImplTest, FindDidSelectAllMatchesWithNextCall) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(0, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext); |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(1, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext); |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(2, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext); |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(0, fake_delegate_.state()->index); |
| } |
| } |
| |
| // Tests that Find in Page selects all matches in a page with three matches and |
| // wraps when making successive FindInPagePrevious calls. |
| TEST_F(FindInPageManagerImplTest, |
| FindDidLoopThroughAllMatchesWithPreviousCall) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| EXPECT_EQ(0, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious); |
| |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_EQ(2, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious); |
| |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_EQ(1, fake_delegate_.state()->index); |
| |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPagePrevious); |
| |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_EQ(0, fake_delegate_.state()->index); |
| } |
| } |
| |
| // Tests that Find in Page does not respond to a FindInPageNext or a |
| // FindInPagePrevious call if no FindInPageSearch find was executed beforehand. |
| TEST_F(FindInPageManagerImplTest, FindDidNotRepondToNextOrPrevIfNoSearch) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| |
| 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 resets the match count to 0 and the query to nil |
| // after calling StopFinding(). |
| TEST_F(FindInPageManagerImplTest, FindInPageCanStopFind) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| |
| fake_delegate_.Reset(); |
| GetFindInPageManager()->StopFinding(); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_FALSE(fake_delegate_.state()->query); |
| EXPECT_EQ(0, fake_delegate_.state()->match_count); |
| } |
| } |
| |
| // Tests that Find in Page logs correct UserActions for given API calls. |
| TEST_F(FindInPageManagerImplTest, FindUserActions) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| ASSERT_EQ( |
| 0, user_action_tester_.GetActionCount("IOS.FindInPage.SearchStarted")); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| |
| ASSERT_EQ(0, user_action_tester_.GetActionCount("IOS.FindInPage.FindNext")); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageNext); |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForIndexOrTimeout(^(int index) { |
| return index == 1; |
| })); |
| 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); |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForIndexOrTimeout(^(int index) { |
| return index == 0; |
| })); |
| EXPECT_EQ( |
| 1, user_action_tester_.GetActionCount("IOS.FindInPage.FindPrevious")); |
| } |
| } |
| |
| // Tests that the Find navigator is presented when the Find session starts and |
| // dismissed when the Find session stops, if a Find interaction is used. |
| TEST_F(FindInPageManagerImplTest, FindNavigatorPresentedAndDismissed) { |
| if (@available(iOS 16, *)) { |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForFindCompletionTimeout, ^{ |
| base::RunLoop().RunUntilIdle(); |
| return fake_web_state_->GetFindInteraction().findNavigatorVisible; |
| })); |
| |
| GetFindInPageManager()->StopFinding(); |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForFindCompletionTimeout, ^{ |
| base::RunLoop().RunUntilIdle(); |
| return !fake_web_state_->GetFindInteraction().findNavigatorVisible; |
| })); |
| } |
| } |
| |
| // Tests that the manager reports to its delegate when it detects the Find |
| // navigator has been dismissed by the user, and set the query back to nil and |
| // the match count to 0. |
| TEST_F(FindInPageManagerImplTest, |
| UserDismissesFindNavigatorDetectedAndStopsSearch) { |
| if (@available(iOS 16, *)) { |
| SetFindSessionWithResultCountsForQueries(@{@"foo" : @3}); |
| GetFindInPageManager()->Find(@"foo", FindInPageOptions::FindInPageSearch); |
| ASSERT_TRUE(WaitForValidIndexOrTimeout()); |
| ASSERT_FALSE(fake_delegate_.state()->user_dismissed_find_navigator); |
| ASSERT_EQ(3, fake_delegate_.state()->match_count); |
| |
| [fake_web_state_->GetFindInteraction() dismissFindNavigator]; |
| fake_delegate_.Reset(); |
| ASSERT_TRUE(WaitForStateOrTimeout()); |
| EXPECT_TRUE(fake_delegate_.state()->user_dismissed_find_navigator); |
| EXPECT_EQ(0, fake_delegate_.state()->match_count); |
| EXPECT_FALSE(fake_delegate_.state()->query); |
| } |
| } |
| |
| } // namespace web |