[CherryPick][iOS] Handle JavaScript Responses in CRWTextFragmentsHandler

Set-up response creation in JavaScript and handling in
CRWTextFragmentsHandler, allowing the collection of success/failure
metrics and measurement of the JavaScript library's rate of success.

(cherry picked from commit ff013a43d963dcc357eb8b67c1a448fc689629d8)

Bug: 1131107
Change-Id: I01eea5d85e2827a496b011d116592bebb74bd92e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2448683
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@{#814791}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2475573
Reviewed-by: Sebastien Lalancette <[email protected]>
Cr-Commit-Position: refs/branch-heads/4280@{#411}
Cr-Branched-From: ea420fb963f9658c9969b6513c56b8f47efa1a2a-refs/heads/master@{#812852}
diff --git a/ios/web/navigation/crw_text_fragments_handler.h b/ios/web/navigation/crw_text_fragments_handler.h
index bd509e2..2977137 100644
--- a/ios/web/navigation/crw_text_fragments_handler.h
+++ b/ios/web/navigation/crw_text_fragments_handler.h
@@ -20,6 +20,7 @@
 // WebStates' loaded URLs.
 @interface CRWTextFragmentsHandler : CRWWebViewHandler
 
+// Initializes a handler with a |delegate| to retrieve the current WebState.
 - (instancetype)initWithDelegate:(id<CRWWebViewHandlerDelegate>)delegate;
 
 // Checks the WebState's destination URL for Text Fragments. If found, searches
diff --git a/ios/web/navigation/crw_text_fragments_handler.mm b/ios/web/navigation/crw_text_fragments_handler.mm
index 83ca273..f127c57 100644
--- a/ios/web/navigation/crw_text_fragments_handler.mm
+++ b/ios/web/navigation/crw_text_fragments_handler.mm
@@ -9,17 +9,26 @@
 #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/js_messaging/web_frame.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"
+#import "ios/web/web_state/web_state_impl.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
 #error "This file requires ARC support."
 #endif
 
-@interface CRWTextFragmentsHandler ()
+namespace {
+
+const char kScriptCommandPrefix[] = "textFragments";
+const char kScriptResponseCommand[] = "textFragments.response";
+
+}  // namespace
+
+@interface CRWTextFragmentsHandler () {
+  std::unique_ptr<web::WebState::ScriptCommandSubscription> _subscription;
+}
 
 @property(nonatomic, weak) id<CRWWebViewHandlerDelegate> delegate;
 
@@ -31,8 +40,24 @@
 @implementation CRWTextFragmentsHandler
 
 - (instancetype)initWithDelegate:(id<CRWWebViewHandlerDelegate>)delegate {
+  DCHECK(delegate);
   if (self = [super init]) {
     _delegate = delegate;
+
+    if (base::FeatureList::IsEnabled(web::features::kScrollToTextIOS) &&
+        self.webStateImpl) {
+      __weak __typeof(self) weakSelf = self;
+      const web::WebState::ScriptCommandCallback callback = base::BindRepeating(
+          ^(const base::DictionaryValue& message, const GURL& page_url,
+            bool interacted, web::WebFrame* sender_frame) {
+            if (sender_frame && sender_frame->IsMainFrame()) {
+              [weakSelf didReceiveJavaScriptResponse:message];
+            }
+          });
+
+      _subscription = self.webStateImpl->AddScriptCommandCallback(
+          callback, kScriptCommandPrefix);
+    }
   }
 
   return self;
@@ -50,6 +75,7 @@
   if (parsedFragments.type() == base::Value::Type::NONE)
     return;
 
+  // TODO (crbug.com/1099268): Log the origin and number of fragments metrics.
   std::string fragmentParam;
   base::JSONWriter::Write(parsedFragments, &fragmentParam);
 
@@ -89,4 +115,13 @@
   return [self.delegate webStateImplForWebViewHandler:self];
 }
 
+- (void)didReceiveJavaScriptResponse:(const base::DictionaryValue&)response {
+  const std::string* command = response.FindStringKey("command");
+  if (!command || *command != kScriptResponseCommand) {
+    return;
+  }
+
+  // TODO (crbug.com/1099268): Log the success/failure metric.
+}
+
 @end
diff --git a/ios/web/navigation/crw_text_fragments_handler_unittest.mm b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
index 9b130c0..463440c 100644
--- a/ios/web/navigation/crw_text_fragments_handler_unittest.mm
+++ b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
@@ -43,6 +43,23 @@
 
   MOCK_METHOD1(ExecuteJavaScript, void(const base::string16&));
   MOCK_CONST_METHOD0(GetLastCommittedURL, const GURL&());
+
+  std::unique_ptr<web::WebState::ScriptCommandSubscription>
+  AddScriptCommandCallback(const web::WebState::ScriptCommandCallback& callback,
+                           const std::string& command_prefix) override {
+    last_callback_ = callback;
+    last_command_prefix_ = command_prefix;
+    return nil;
+  }
+
+  web::WebState::ScriptCommandCallback last_callback() {
+    return last_callback_;
+  }
+  const std::string last_command_prefix() { return last_command_prefix_; }
+
+ private:
+  web::WebState::ScriptCommandCallback last_callback_;
+  std::string last_command_prefix_;
 };
 
 class CRWTextFragmentsHandlerTest : public web::WebTest {
@@ -109,6 +126,11 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(expected_javascript)).Times(1);
 
   [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+
+  // Verify that a command callback was added with the right prefix.
+  EXPECT_NE(web::WebState::ScriptCommandCallback(),
+            web_state_->last_callback());
+  EXPECT_EQ("textFragments", web_state_->last_command_prefix());
 }
 
 // Tests that the handler will not execute JavaScript if the scroll to text
@@ -123,6 +145,10 @@
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
   [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+
+  // Verify that no callback was set when the flag is disabled.
+  EXPECT_EQ(web::WebState::ScriptCommandCallback(),
+            web_state_->last_callback());
 }
 
 // Tests that the handler will not execute JavaScript if the WebState has an
diff --git a/ios/web/web_state/js/resources/text_fragments.js b/ios/web/web_state/js/resources/text_fragments.js
index 2c775d5..d5bd4405 100644
--- a/ios/web/web_state/js/resources/text_fragments.js
+++ b/ios/web/web_state/js/resources/text_fragments.js
@@ -37,27 +37,38 @@
    * Does the actual work for handleTextFragments.
    */
   const doHandleTextFragments = function(fragments, scroll) {
-    let marks = [];
+    const marks = [];
+    let successCount = 0;
 
     for (const fragment of fragments) {
       // Process the fragments, then filter out any that evaluate to false.
-      let newMarks = utils.processTextFragmentDirective(fragment)
+      const newMarks = utils.processTextFragmentDirective(fragment)
           .filter((mark) => { return !!mark });
 
-      if (Array.isArray(newMarks))
+      if (Array.isArray(newMarks)) {
+        if (newMarks.length > 0) {
+          ++successCount;
+        }
+
         marks.push(...newMarks);
+      }
     }
 
     if (scroll && marks.length > 0)
       utils.scrollElementIntoView(marks[0]);
 
-    // TODO(crbug.com/1099268): Count successes/failures above and log metrics
-
     for (const mark of marks) {
       mark.addEventListener("click", () => {
         utils.removeMarks(marks);
       });
     }
-  }
 
+    __gCrWeb.message.invokeOnHost({
+      command: 'textFragments.response',
+      result: {
+        successCount: successCount,
+        fragmentsCount: fragments.length
+      }
+    });
+  }
 })();