[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