[CherryPick][iOS] Create the TextFragmentsHandler Class

Moving some of the logic that used to live in utils into a new handler
class. This class will be used to receive asynchronous JavaScript
responses in a subsequent CL, hence the requirement to use an instance
rather than utility functions.

(cherry picked from commit fcc4e7066e34f5bd10fdec69ef41febb239617c1)

Bug: 1131107
Change-Id: I6752d1532bc5e64d40f327b7506e298d136e78a9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2446816
Commit-Queue: Sebastien Lalancette <[email protected]>
Reviewed-by: Eugene But <[email protected]>
Reviewed-by: Tommy Martino <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#814304}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2470466
Reviewed-by: Sebastien Lalancette <[email protected]>
Cr-Commit-Position: refs/branch-heads/4280@{#410}
Cr-Branched-From: ea420fb963f9658c9969b6513c56b8f47efa1a2a-refs/heads/master@{#812852}
diff --git a/ios/web/BUILD.gn b/ios/web/BUILD.gn
index b53e65c..1293f36 100644
--- a/ios/web/BUILD.gn
+++ b/ios/web/BUILD.gn
@@ -251,6 +251,7 @@
     "//ios/web/test:test_support",
     "//ios/web/test/fakes",
     "//ios/web/web_state/ui:crw_web_view_navigation_proxy",
+    "//ios/web/web_state/ui:web_view_handler",
     "//net:test_support",
     "//testing/gmock",
     "//testing/gtest",
@@ -261,6 +262,7 @@
   sources = [
     "navigation/crw_navigation_item_holder_unittest.mm",
     "navigation/crw_session_storage_unittest.mm",
+    "navigation/crw_text_fragments_handler_unittest.mm",
     "navigation/crw_wk_navigation_states_unittest.mm",
     "navigation/error_page_helper_unittest.mm",
     "navigation/error_retry_state_machine_unittest.mm",
@@ -271,7 +273,7 @@
     "navigation/navigation_manager_util_unittest.mm",
     "navigation/nscoder_util_unittest.mm",
     "navigation/session_storage_builder_unittest.mm",
-    "navigation/text_fragment_utils_unittest.mm",
+    "navigation/text_fragments_utils_unittest.mm",
     "navigation/wk_back_forward_list_item_holder_unittest.mm",
     "navigation/wk_based_navigation_manager_impl_unittest.mm",
     "navigation/wk_navigation_action_policy_util_unittest.mm",
diff --git a/ios/web/navigation/BUILD.gn b/ios/web/navigation/BUILD.gn
index 2b6252b..715ad5c4 100644
--- a/ios/web/navigation/BUILD.gn
+++ b/ios/web/navigation/BUILD.gn
@@ -47,6 +47,8 @@
     "crw_navigation_item_holder.mm",
     "crw_pending_navigation_info.h",
     "crw_pending_navigation_info.mm",
+    "crw_text_fragments_handler.h",
+    "crw_text_fragments_handler.mm",
     "crw_web_view_navigation_observer.h",
     "crw_web_view_navigation_observer.mm",
     "crw_web_view_navigation_observer_delegate.h",
@@ -62,8 +64,8 @@
     "serializable_user_data_manager_impl.mm",
     "session_storage_builder.h",
     "session_storage_builder.mm",
-    "text_fragment_utils.h",
-    "text_fragment_utils.mm",
+    "text_fragments_utils.h",
+    "text_fragments_utils.mm",
     "time_smoother.cc",
     "time_smoother.h",
     "url_schemes.mm",
diff --git a/ios/web/navigation/crw_text_fragments_handler.h b/ios/web/navigation/crw_text_fragments_handler.h
new file mode 100644
index 0000000..bd509e2
--- /dev/null
+++ b/ios/web/navigation/crw_text_fragments_handler.h
@@ -0,0 +1,34 @@
+// Copyright 2020 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.
+
+#ifndef IOS_WEB_NAVIGATION_CRW_TEXT_FRAGMENTS_HANDLER_H_
+#define IOS_WEB_NAVIGATION_CRW_TEXT_FRAGMENTS_HANDLER_H_
+
+#import <UIKit/UIKit.h>
+
+#import "ios/web/web_state/ui/crw_web_view_handler.h"
+
+@protocol CRWWebViewHandlerDelegate;
+
+namespace web {
+class NavigationContext;
+struct Referrer;
+}
+
+// Class in charge of highlighting text fragments when they are present in
+// WebStates' loaded URLs.
+@interface CRWTextFragmentsHandler : CRWWebViewHandler
+
+- (instancetype)initWithDelegate:(id<CRWWebViewHandlerDelegate>)delegate;
+
+// Checks the WebState's destination URL for Text Fragments. If found, searches
+// the DOM for matching text, highlights the text, and scrolls the first into
+// view. Uses the |context| and |referrer| to analyze the current navigation
+// scenario.
+- (void)processTextFragmentsWithContext:(web::NavigationContext*)context
+                               referrer:(web::Referrer)referrer;
+
+@end
+
+#endif  // IOS_WEB_NAVIGATION_CRW_TEXT_FRAGMENTS_HANDLER_H_
diff --git a/ios/web/navigation/crw_text_fragments_handler.mm b/ios/web/navigation/crw_text_fragments_handler.mm
new file mode 100644
index 0000000..83ca273
--- /dev/null
+++ b/ios/web/navigation/crw_text_fragments_handler.mm
@@ -0,0 +1,92 @@
+// Copyright 2020 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.
+
+#import "ios/web/navigation/crw_text_fragments_handler.h"
+
+#import "base/json/json_writer.h"
+#import "base/strings/string_util.h"
+#import "base/strings/utf_string_conversions.h"
+#import "ios/web/common/features.h"
+#import "ios/web/navigation/text_fragments_utils.h"
+#import "ios/web/public/navigation/navigation_context.h"
+#import "ios/web/public/navigation/referrer.h"
+#import "ios/web/web_state/web_state_impl.h"
+
+#import "ios/web/web_state/ui/crw_web_view_handler_delegate.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@interface CRWTextFragmentsHandler ()
+
+@property(nonatomic, weak) id<CRWWebViewHandlerDelegate> delegate;
+
+// Returns the WebStateImpl from self.delegate.
+@property(nonatomic, readonly, assign) web::WebStateImpl* webStateImpl;
+
+@end
+
+@implementation CRWTextFragmentsHandler
+
+- (instancetype)initWithDelegate:(id<CRWWebViewHandlerDelegate>)delegate {
+  if (self = [super init]) {
+    _delegate = delegate;
+  }
+
+  return self;
+}
+
+- (void)processTextFragmentsWithContext:(web::NavigationContext*)context
+                               referrer:(web::Referrer)referrer {
+  if (!context || ![self areTextFragmentsAllowedInContext:context]) {
+    return;
+  }
+
+  base::Value parsedFragments =
+      web::ParseTextFragments(self.webStateImpl->GetLastCommittedURL());
+
+  if (parsedFragments.type() == base::Value::Type::NONE)
+    return;
+
+  std::string fragmentParam;
+  base::JSONWriter::Write(parsedFragments, &fragmentParam);
+
+  std::string script = base::ReplaceStringPlaceholders(
+      "__gCrWeb.textFragments.handleTextFragments($1, $2)",
+      {fragmentParam, /* scroll = */ "true"},
+      /* offsets= */ nil);
+
+  self.webStateImpl->ExecuteJavaScript(base::UTF8ToUTF16(script));
+}
+
+#pragma mark - Private Methods
+
+// Returns NO if fragments highlighting is not allowed in the current |context|.
+- (BOOL)areTextFragmentsAllowedInContext:(web::NavigationContext*)context {
+  if (!base::FeatureList::IsEnabled(web::features::kScrollToTextIOS))
+    return NO;
+
+  if (self.isBeingDestroyed) {
+    return NO;
+  }
+
+  // If the current instance isn't being destroyed, then it must be able to get
+  // a valid WebState.
+  DCHECK(self.webStateImpl);
+
+  if (self.webStateImpl->HasOpener()) {
+    // TODO(crbug.com/1099268): Loosen this restriction if the opener has the
+    // same domain.
+    return NO;
+  }
+
+  return context->HasUserGesture() && !context->IsSameDocument();
+}
+
+- (web::WebStateImpl*)webStateImpl {
+  return [self.delegate webStateImplForWebViewHandler:self];
+}
+
+@end
diff --git a/ios/web/navigation/crw_text_fragments_handler_unittest.mm b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
new file mode 100644
index 0000000..9b130c0
--- /dev/null
+++ b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
@@ -0,0 +1,202 @@
+// Copyright 2020 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.
+
+#import "ios/web/navigation/crw_text_fragments_handler.h"
+
+#import "base/strings/utf_string_conversions.h"
+#import "base/test/scoped_feature_list.h"
+#import "ios/web/common/features.h"
+#import "ios/web/navigation/text_fragments_utils.h"
+#import "ios/web/public/navigation/referrer.h"
+#import "ios/web/public/test/fakes/fake_navigation_context.h"
+#import "ios/web/public/test/web_test.h"
+#import "ios/web/web_state/ui/crw_web_view_handler_delegate.h"
+#import "ios/web/web_state/web_state_impl.h"
+#import "testing/gmock/include/gmock/gmock.h"
+#import "testing/gtest/include/gtest/gtest.h"
+#import "third_party/ocmock/OCMock/OCMock.h"
+#import "url/gurl.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+using web::Referrer;
+using ::testing::_;
+using ::testing::ReturnRefOfCopy;
+
+namespace {
+
+const char kValidFragmentsURL[] =
+    "https://chromium.org/#idFrag:~:text=text%201&text=text%202";
+const char kScriptForValidFragmentsURL[] =
+    "__gCrWeb.textFragments.handleTextFragments([{\"textStart\":\"text "
+    "1\"},{\"textStart\":\"text 2\"}], true)";
+
+}  // namespace
+
+class MockWebStateImpl : public web::WebStateImpl {
+ public:
+  explicit MockWebStateImpl(web::WebState::CreateParams params)
+      : web::WebStateImpl(params) {}
+
+  MOCK_METHOD1(ExecuteJavaScript, void(const base::string16&));
+  MOCK_CONST_METHOD0(GetLastCommittedURL, const GURL&());
+};
+
+class CRWTextFragmentsHandlerTest : public web::WebTest {
+ protected:
+  CRWTextFragmentsHandlerTest() : context_(), feature_list_() {}
+
+  void SetUp() override {
+    web::WebState::CreateParams params(GetBrowserState());
+    std::unique_ptr<MockWebStateImpl> web_state =
+        std::make_unique<MockWebStateImpl>(params);
+    web_state_ = web_state.get();
+    context_.SetWebState(std::move(web_state));
+
+    mocked_delegate_ =
+        OCMStrictProtocolMock(@protocol(CRWWebViewHandlerDelegate));
+    OCMStub([mocked_delegate_ webStateImplForWebViewHandler:[OCMArg any]])
+        .andReturn((web::WebStateImpl*)web_state_);
+  }
+
+  CRWTextFragmentsHandler* CreateDefaultHandler() {
+    return CreateHandler(/*has_opener=*/false,
+                         /*has_user_gesture=*/true,
+                         /*is_same_document=*/false,
+                         /*feature_enabled=*/true);
+  }
+
+  CRWTextFragmentsHandler* CreateHandler(bool has_opener,
+                                         bool has_user_gesture,
+                                         bool is_same_document,
+                                         bool feature_enabled) {
+    if (feature_enabled) {
+      feature_list_.InitAndEnableFeature(web::features::kScrollToTextIOS);
+    } else {
+      feature_list_.InitAndDisableFeature(web::features::kScrollToTextIOS);
+    }
+    web_state_->SetHasOpener(has_opener);
+    context_.SetHasUserGesture(has_user_gesture);
+    context_.SetIsSameDocument(is_same_document);
+
+    return [[CRWTextFragmentsHandler alloc] initWithDelegate:mocked_delegate_];
+  }
+
+  void SetLastURL(const GURL& last_url) {
+    EXPECT_CALL(*web_state_, GetLastCommittedURL())
+        .WillOnce(ReturnRefOfCopy(last_url));
+  }
+
+  web::FakeNavigationContext context_;
+  MockWebStateImpl* web_state_;
+  base::test::ScopedFeatureList feature_list_;
+  id<CRWWebViewHandlerDelegate> mocked_delegate_;
+};
+
+// Tests that the handler will execute JavaScript if highlighting is allowed and
+// fragments are present.
+TEST_F(CRWTextFragmentsHandlerTest, ExecuteJavaScriptSuccess) {
+  SetLastURL(GURL(kValidFragmentsURL));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  // Set up expectation.
+  base::string16 expected_javascript =
+      base::UTF8ToUTF16(kScriptForValidFragmentsURL);
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(expected_javascript)).Times(1);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that the handler will not execute JavaScript if the scroll to text
+// feature is disabled.
+TEST_F(CRWTextFragmentsHandlerTest, FeatureDisabledFragmentsDisallowed) {
+  CRWTextFragmentsHandler* handler = CreateHandler(/*has_opener=*/false,
+                                                   /*has_user_gesture=*/true,
+                                                   /*is_same_document=*/false,
+                                                   /*feature_enabled=*/false);
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+  EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that the handler will not execute JavaScript if the WebState has an
+// opener.
+TEST_F(CRWTextFragmentsHandlerTest, HasOpenerFragmentsDisallowed) {
+  CRWTextFragmentsHandler* handler = CreateHandler(/*has_opener=*/true,
+                                                   /*has_user_gesture=*/true,
+                                                   /*is_same_document=*/false,
+                                                   /*feature_enabled=*/true);
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+  EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that the handler will not execute JavaScript if the WebState has no
+// user gesture.
+TEST_F(CRWTextFragmentsHandlerTest, NoGestureFragmentsDisallowed) {
+  CRWTextFragmentsHandler* handler = CreateHandler(/*has_opener=*/false,
+                                                   /*has_user_gesture=*/false,
+                                                   /*is_same_document=*/false,
+                                                   /*feature_enabled=*/true);
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+  EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that the handler will not execute JavaScript if we navigated on the
+// same document.
+TEST_F(CRWTextFragmentsHandlerTest, SameDocumentFragmentsDisallowed) {
+  CRWTextFragmentsHandler* handler = CreateHandler(/*has_opener=*/false,
+                                                   /*has_user_gesture=*/true,
+                                                   /*is_same_document=*/true,
+                                                   /*feature_enabled=*/true);
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+  EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that the handler will not execute JavaScript if there are no
+// fragments on the current URL.
+TEST_F(CRWTextFragmentsHandlerTest, NoFragmentsNoJavaScript) {
+  SetLastURL(GURL("https://www.chromium.org/"));
+
+  CRWTextFragmentsHandler* handler = CreateHandler(/*has_opener=*/false,
+                                                   /*has_user_gesture=*/true,
+                                                   /*is_same_document=*/false,
+                                                   /*feature_enabled=*/true);
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
+
+// Tests that any timing issue which would call the handle after it got closed
+// would not crash the app.
+TEST_F(CRWTextFragmentsHandlerTest, PostCloseInvokeDoesNotCrash) {
+  // Reset the mock.
+  mocked_delegate_ =
+      OCMStrictProtocolMock(@protocol(CRWWebViewHandlerDelegate));
+  OCMStub([mocked_delegate_ webStateImplForWebViewHandler:[OCMArg any]])
+      .andReturn((web::WebStateImpl*)nullptr);
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler close];
+
+  EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
+  EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
+
+  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+}
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 63a6faf..05209f2 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -18,13 +18,13 @@
 #import "ios/web/js_messaging/web_frames_manager_impl.h"
 #import "ios/web/navigation/crw_navigation_item_holder.h"
 #import "ios/web/navigation/crw_pending_navigation_info.h"
+#import "ios/web/navigation/crw_text_fragments_handler.h"
 #import "ios/web/navigation/crw_wk_navigation_states.h"
 #import "ios/web/navigation/error_page_helper.h"
 #include "ios/web/navigation/error_retry_state_machine.h"
 #import "ios/web/navigation/navigation_context_impl.h"
 #import "ios/web/navigation/navigation_manager_impl.h"
 #include "ios/web/navigation/navigation_manager_util.h"
-#import "ios/web/navigation/text_fragment_utils.h"
 #import "ios/web/navigation/web_kit_constants.h"
 #import "ios/web/navigation/wk_back_forward_list_item_holder.h"
 #import "ios/web/navigation/wk_navigation_action_policy_util.h"
@@ -116,6 +116,8 @@
 @property(nonatomic, readonly, assign) GURL documentURL;
 // Returns the js injector from self.delegate.
 @property(nonatomic, readonly, weak) CRWJSInjector* JSInjector;
+// Will handle highlighting text fragments on the page when necessary.
+@property(nonatomic, strong) CRWTextFragmentsHandler* textFragmentsHandler;
 
 @end
 
@@ -133,6 +135,9 @@
             kMaxCertErrorsCount);
 
     _delegate = delegate;
+
+    _textFragmentsHandler =
+        [[CRWTextFragmentsHandler alloc] initWithDelegate:_delegate];
   }
   return self;
 }
@@ -1149,9 +1154,9 @@
     }
   }
 
-  if (context && web::AreTextFragmentsAllowed(context)) {
-    web::HandleTextFragments(self.webStateImpl);
-  }
+  [self.textFragmentsHandler
+      processTextFragmentsWithContext:context
+                             referrer:self.currentReferrer];
 
   [self.navigationStates setState:web::WKNavigationState::FINISHED
                     forNavigation:navigation];
diff --git a/ios/web/navigation/text_fragment_utils_unittest.mm b/ios/web/navigation/text_fragment_utils_unittest.mm
deleted file mode 100644
index 502c43d..0000000
--- a/ios/web/navigation/text_fragment_utils_unittest.mm
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright 2020 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.
-
-#import "ios/web/navigation/text_fragment_utils.h"
-
-#include <memory>
-
-#include "base/test/scoped_feature_list.h"
-#include "ios/web/common/features.h"
-#import "ios/web/public/test/fakes/fake_navigation_context.h"
-#import "ios/web/public/test/fakes/test_web_state.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "testing/platform_test.h"
-#include "url/gurl.h"
-
-#if !defined(__has_feature) || !__has_feature(objc_arc)
-#error "This file requires ARC support."
-#endif
-
-namespace {
-
-// These values correspond to the members that the JavaScript implementation is
-// expecting.
-const char kPrefixKey[] = "prefix";
-const char kTextStartKey[] = "textStart";
-const char kTextEndKey[] = "textEnd";
-const char kSuffixKey[] = "suffix";
-
-}  // namespace
-
-namespace web {
-
-typedef PlatformTest TextFragmentUtilsTest;
-
-TEST_F(TextFragmentUtilsTest, AreTextFragmentsAllowed) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(features::kScrollToTextIOS);
-
-  std::unique_ptr<TestWebState> web_state = std::make_unique<TestWebState>();
-  TestWebState* web_state_ptr = web_state.get();
-  FakeNavigationContext context;
-  context.SetWebState(std::move(web_state));
-
-  // Working case: no opener, has user gesture, not same document
-  web_state_ptr->SetHasOpener(false);
-  context.SetHasUserGesture(true);
-  context.SetIsSameDocument(false);
-  EXPECT_TRUE(AreTextFragmentsAllowed(&context));
-
-  // Blocking case #1: WebState has an opener
-  web_state_ptr->SetHasOpener(true);
-  context.SetHasUserGesture(true);
-  context.SetIsSameDocument(false);
-  EXPECT_FALSE(AreTextFragmentsAllowed(&context));
-
-  // Blocking case #2: No user gesture
-  web_state_ptr->SetHasOpener(false);
-  context.SetHasUserGesture(false);
-  context.SetIsSameDocument(false);
-  EXPECT_FALSE(AreTextFragmentsAllowed(&context));
-
-  // Blocking case #3: Same-document navigation
-  web_state_ptr->SetHasOpener(false);
-  context.SetHasUserGesture(true);
-  context.SetIsSameDocument(true);
-  EXPECT_FALSE(AreTextFragmentsAllowed(&context));
-}
-
-TEST_F(TextFragmentUtilsTest, ParseTextFragments) {
-  GURL url_with_fragment(
-      "https://www.example.com/#idFrag:~:text=text%201&text=text%202");
-  base::Value result = internal::ParseTextFragments(url_with_fragment);
-  ASSERT_EQ(2u, result.GetList().size());
-  EXPECT_EQ("text 1", result.GetList()[0].FindKey(kTextStartKey)->GetString());
-  EXPECT_EQ("text 2", result.GetList()[1].FindKey(kTextStartKey)->GetString());
-
-  GURL url_no_fragment("www.example.com");
-  base::Value empty_result = internal::ParseTextFragments(url_no_fragment);
-  EXPECT_TRUE(empty_result.is_none());
-}
-
-TEST_F(TextFragmentUtilsTest, ExtractTextFragments) {
-  std::vector<std::string> expected = {"test1", "test2", "test3"};
-  // Ensure presence/absence of a trailing & doesn't break anything
-  EXPECT_EQ(expected, internal::ExtractTextFragments(
-                          "#id:~:text=test1&text=test2&text=test3"));
-  EXPECT_EQ(expected, internal::ExtractTextFragments(
-                          "#id:~:text=test1&text=test2&text=test3&"));
-
-  // Test that empty tokens (&& or &text=&) are discarded
-  EXPECT_EQ(expected, internal::ExtractTextFragments(
-                          "#id:~:text=test1&&text=test2&text=&text=test3"));
-
-  expected = {};
-  EXPECT_EQ(expected,
-            internal::ExtractTextFragments("#idButNoTextFragmentsHere"));
-  EXPECT_EQ(expected, internal::ExtractTextFragments(""));
-}
-
-TEST_F(TextFragmentUtilsTest, TextFragmentToValue) {
-  // Success cases
-  std::string fragment = "start";
-  base::Value result = internal::TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kTextEndKey));
-  EXPECT_FALSE(result.FindKey(kSuffixKey));
-
-  fragment = "start,end";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kSuffixKey));
-
-  fragment = "prefix-,start";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kTextEndKey));
-  EXPECT_FALSE(result.FindKey(kSuffixKey));
-
-  fragment = "start,-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kTextEndKey));
-  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
-
-  fragment = "prefix-,start,end";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kSuffixKey));
-
-  fragment = "start,end,-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
-  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
-
-  fragment = "prefix-,start,end,-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
-  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
-
-  // Trailing comma doesn't break otherwise valid fragment
-  fragment = "start,";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kTextEndKey));
-  EXPECT_FALSE(result.FindKey(kSuffixKey));
-
-  // Failure Cases
-  fragment = "";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "some,really-,malformed,-thing,with,too,many,commas";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "prefix-,-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "start,prefix-,-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "prefix-,-suffix,start";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "prefix-";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-
-  fragment = "-suffix";
-  result = internal::TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
-}
-
-}  // namespace web
diff --git a/ios/web/navigation/text_fragment_utils.h b/ios/web/navigation/text_fragments_utils.h
similarity index 61%
rename from ios/web/navigation/text_fragment_utils.h
rename to ios/web/navigation/text_fragments_utils.h
index 1183629..bdeffdd 100644
--- a/ios/web/navigation/text_fragment_utils.h
+++ b/ios/web/navigation/text_fragments_utils.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_
-#define IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_
+#ifndef IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
+#define IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
 
 #include "base/values.h"
 
@@ -11,26 +11,11 @@
 
 namespace web {
 
-class NavigationContext;
-class WebState;
-
 // This file contains helper functions relating to Text Fragments, which are
 // appended to the reference fragment in the URL and instruct the user agent
 // to highlight a given snippet of text and the page and scroll it into view.
 // See also: https://wicg.github.io/scroll-to-text-fragment/
 
-// Checks if product and security requirements permit the use of Text Fragments.
-// Does not guarantee that the URL contains a Text Fragment or that the matching
-// text will be found on the page.
-bool AreTextFragmentsAllowed(NavigationContext* context);
-
-// Checks the destination URL for Text Fragments. If found, searches the DOM for
-// matching text, highlights the text, and scrolls the first into view.
-void HandleTextFragments(WebState* state);
-
-// Exposed for testing only.
-namespace internal {
-
 // Checks the fragment portion of the URL for Text Fragments. Returns zero or
 // more dictionaries containing the parsed parameters used by the fragment-
 // finding algorithm, as defined in the spec.
@@ -44,7 +29,6 @@
 // fragment is malformed.
 base::Value TextFragmentToValue(std::string fragment);
 
-}  // namespace internal
 }  // namespace web
 
-#endif  // IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_
+#endif  // IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
diff --git a/ios/web/navigation/text_fragment_utils.mm b/ios/web/navigation/text_fragments_utils.mm
similarity index 81%
rename from ios/web/navigation/text_fragment_utils.mm
rename to ios/web/navigation/text_fragments_utils.mm
index e41215e..9fb6902 100644
--- a/ios/web/navigation/text_fragment_utils.mm
+++ b/ios/web/navigation/text_fragments_utils.mm
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#import "ios/web/navigation/text_fragment_utils.h"
+#import "ios/web/navigation/text_fragments_utils.h"
 
 #include <cstring.h>
 
@@ -32,40 +32,6 @@
 
 namespace web {
 
-bool AreTextFragmentsAllowed(NavigationContext* context) {
-  if (!base::FeatureList::IsEnabled(features::kScrollToTextIOS))
-    return false;
-
-  WebState* web_state = context->GetWebState();
-  if (web_state->HasOpener()) {
-    // TODO(crbug.com/1099268): Loosen this restriction if the opener has the
-    // same domain.
-    return false;
-  }
-
-  return context->HasUserGesture() && !context->IsSameDocument();
-}
-
-void HandleTextFragments(WebState* state) {
-  base::Value parsed_fragments =
-      internal::ParseTextFragments(state->GetLastCommittedURL());
-
-  if (parsed_fragments.type() == base::Value::Type::NONE)
-    return;
-
-  std::string fragment_param;
-  base::JSONWriter::Write(parsed_fragments, &fragment_param);
-
-  std::string script = base::ReplaceStringPlaceholders(
-      "__gCrWeb.textFragments.handleTextFragments($1, $2)",
-      {fragment_param, /* scroll = */ "true"},
-      /* offsets= */ nullptr);
-
-  state->ExecuteJavaScript(base::UTF8ToUTF16(script));
-}
-
-namespace internal {
-
 base::Value ParseTextFragments(const GURL& url) {
   if (!url.has_ref())
     return {};
@@ -171,5 +137,4 @@
   return dict;
 }
 
-}  // namespace internal
 }  // namespace web
diff --git a/ios/web/navigation/text_fragments_utils_unittest.mm b/ios/web/navigation/text_fragments_utils_unittest.mm
new file mode 100644
index 0000000..b06c6a3
--- /dev/null
+++ b/ios/web/navigation/text_fragments_utils_unittest.mm
@@ -0,0 +1,149 @@
+// Copyright 2020 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.
+
+#import "ios/web/navigation/text_fragments_utils.h"
+
+#import "testing/gtest/include/gtest/gtest.h"
+#import "testing/platform_test.h"
+#import "url/gurl.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+namespace {
+
+// These values correspond to the members that the JavaScript implementation is
+// expecting.
+const char kPrefixKey[] = "prefix";
+const char kTextStartKey[] = "textStart";
+const char kTextEndKey[] = "textEnd";
+const char kSuffixKey[] = "suffix";
+
+}  // namespace
+
+namespace web {
+
+typedef PlatformTest TextFragmentsUtilsTest;
+
+TEST_F(TextFragmentsUtilsTest, ParseTextFragments) {
+  GURL url_with_fragment(
+      "https://www.example.com/#idFrag:~:text=text%201&text=text%202");
+  base::Value result = ParseTextFragments(url_with_fragment);
+  ASSERT_EQ(2u, result.GetList().size());
+  EXPECT_EQ("text 1", result.GetList()[0].FindKey(kTextStartKey)->GetString());
+  EXPECT_EQ("text 2", result.GetList()[1].FindKey(kTextStartKey)->GetString());
+
+  GURL url_no_fragment("www.example.com");
+  base::Value empty_result = ParseTextFragments(url_no_fragment);
+  EXPECT_TRUE(empty_result.is_none());
+}
+
+TEST_F(TextFragmentsUtilsTest, ExtractTextFragments) {
+  std::vector<std::string> expected = {"test1", "test2", "test3"};
+  // Ensure presence/absence of a trailing & doesn't break anything
+  EXPECT_EQ(expected,
+            ExtractTextFragments("#id:~:text=test1&text=test2&text=test3"));
+  EXPECT_EQ(expected,
+            ExtractTextFragments("#id:~:text=test1&text=test2&text=test3&"));
+
+  // Test that empty tokens (&& or &text=&) are discarded
+  EXPECT_EQ(expected, ExtractTextFragments(
+                          "#id:~:text=test1&&text=test2&text=&text=test3"));
+
+  expected.clear();
+  EXPECT_EQ(expected, ExtractTextFragments("#idButNoTextFragmentsHere"));
+  EXPECT_EQ(expected, ExtractTextFragments(""));
+}
+
+TEST_F(TextFragmentsUtilsTest, TextFragmentToValue) {
+  // Success cases
+  std::string fragment = "start";
+  base::Value result = TextFragmentToValue(fragment);
+  EXPECT_FALSE(result.FindKey(kPrefixKey));
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kTextEndKey));
+  EXPECT_FALSE(result.FindKey(kSuffixKey));
+
+  fragment = "start,end";
+  result = TextFragmentToValue(fragment);
+  EXPECT_FALSE(result.FindKey(kPrefixKey));
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kSuffixKey));
+
+  fragment = "prefix-,start";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kTextEndKey));
+  EXPECT_FALSE(result.FindKey(kSuffixKey));
+
+  fragment = "start,-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_FALSE(result.FindKey(kPrefixKey));
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kTextEndKey));
+  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
+
+  fragment = "prefix-,start,end";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kSuffixKey));
+
+  fragment = "start,end,-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_FALSE(result.FindKey(kPrefixKey));
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
+  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
+
+  fragment = "prefix-,start,end,-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
+  EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
+
+  // Trailing comma doesn't break otherwise valid fragment
+  fragment = "start,";
+  result = TextFragmentToValue(fragment);
+  EXPECT_FALSE(result.FindKey(kPrefixKey));
+  EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
+  EXPECT_FALSE(result.FindKey(kTextEndKey));
+  EXPECT_FALSE(result.FindKey(kSuffixKey));
+
+  // Failure Cases
+  fragment = "";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "some,really-,malformed,-thing,with,too,many,commas";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "prefix-,-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "start,prefix-,-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "prefix-,-suffix,start";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "prefix-";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+
+  fragment = "-suffix";
+  result = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result.type());
+}
+
+}  // namespace web