Commit fcc4e706 authored by Sebastien Lalancette's avatar Sebastien Lalancette Committed by Commit Bot

[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.

Bug: 1099268
Change-Id: I6752d1532bc5e64d40f327b7506e298d136e78a9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2446816
Commit-Queue: Sebastien Lalancette <seblalancette@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarTommy Martino <tmartino@chromium.org>
Cr-Commit-Position: refs/heads/master@{#814304}
parent a60f7247
...@@ -251,6 +251,7 @@ source_set("ios_web_navigation_unittests") { ...@@ -251,6 +251,7 @@ source_set("ios_web_navigation_unittests") {
"//ios/web/test:test_support", "//ios/web/test:test_support",
"//ios/web/test/fakes", "//ios/web/test/fakes",
"//ios/web/web_state/ui:crw_web_view_navigation_proxy", "//ios/web/web_state/ui:crw_web_view_navigation_proxy",
"//ios/web/web_state/ui:web_view_handler",
"//net:test_support", "//net:test_support",
"//testing/gmock", "//testing/gmock",
"//testing/gtest", "//testing/gtest",
...@@ -261,6 +262,7 @@ source_set("ios_web_navigation_unittests") { ...@@ -261,6 +262,7 @@ source_set("ios_web_navigation_unittests") {
sources = [ sources = [
"navigation/crw_navigation_item_holder_unittest.mm", "navigation/crw_navigation_item_holder_unittest.mm",
"navigation/crw_session_storage_unittest.mm", "navigation/crw_session_storage_unittest.mm",
"navigation/crw_text_fragments_handler_unittest.mm",
"navigation/crw_wk_navigation_states_unittest.mm", "navigation/crw_wk_navigation_states_unittest.mm",
"navigation/error_page_helper_unittest.mm", "navigation/error_page_helper_unittest.mm",
"navigation/error_retry_state_machine_unittest.mm", "navigation/error_retry_state_machine_unittest.mm",
...@@ -271,7 +273,7 @@ source_set("ios_web_navigation_unittests") { ...@@ -271,7 +273,7 @@ source_set("ios_web_navigation_unittests") {
"navigation/navigation_manager_util_unittest.mm", "navigation/navigation_manager_util_unittest.mm",
"navigation/nscoder_util_unittest.mm", "navigation/nscoder_util_unittest.mm",
"navigation/session_storage_builder_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_back_forward_list_item_holder_unittest.mm",
"navigation/wk_based_navigation_manager_impl_unittest.mm", "navigation/wk_based_navigation_manager_impl_unittest.mm",
"navigation/wk_navigation_action_policy_util_unittest.mm", "navigation/wk_navigation_action_policy_util_unittest.mm",
......
...@@ -47,6 +47,8 @@ source_set("navigation") { ...@@ -47,6 +47,8 @@ source_set("navigation") {
"crw_navigation_item_holder.mm", "crw_navigation_item_holder.mm",
"crw_pending_navigation_info.h", "crw_pending_navigation_info.h",
"crw_pending_navigation_info.mm", "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.h",
"crw_web_view_navigation_observer.mm", "crw_web_view_navigation_observer.mm",
"crw_web_view_navigation_observer_delegate.h", "crw_web_view_navigation_observer_delegate.h",
...@@ -62,8 +64,8 @@ source_set("navigation") { ...@@ -62,8 +64,8 @@ source_set("navigation") {
"serializable_user_data_manager_impl.mm", "serializable_user_data_manager_impl.mm",
"session_storage_builder.h", "session_storage_builder.h",
"session_storage_builder.mm", "session_storage_builder.mm",
"text_fragment_utils.h", "text_fragments_utils.h",
"text_fragment_utils.mm", "text_fragments_utils.mm",
"time_smoother.cc", "time_smoother.cc",
"time_smoother.h", "time_smoother.h",
"url_schemes.mm", "url_schemes.mm",
......
// 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_
// 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
// 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()];
}
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
#import "ios/web/js_messaging/web_frames_manager_impl.h" #import "ios/web/js_messaging/web_frames_manager_impl.h"
#import "ios/web/navigation/crw_navigation_item_holder.h" #import "ios/web/navigation/crw_navigation_item_holder.h"
#import "ios/web/navigation/crw_pending_navigation_info.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/crw_wk_navigation_states.h"
#import "ios/web/navigation/error_page_helper.h" #import "ios/web/navigation/error_page_helper.h"
#include "ios/web/navigation/error_retry_state_machine.h" #include "ios/web/navigation/error_retry_state_machine.h"
#import "ios/web/navigation/navigation_context_impl.h" #import "ios/web/navigation/navigation_context_impl.h"
#import "ios/web/navigation/navigation_manager_impl.h" #import "ios/web/navigation/navigation_manager_impl.h"
#include "ios/web/navigation/navigation_manager_util.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/web_kit_constants.h"
#import "ios/web/navigation/wk_back_forward_list_item_holder.h" #import "ios/web/navigation/wk_back_forward_list_item_holder.h"
#import "ios/web/navigation/wk_navigation_action_policy_util.h" #import "ios/web/navigation/wk_navigation_action_policy_util.h"
...@@ -116,6 +116,8 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation( ...@@ -116,6 +116,8 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation(
@property(nonatomic, readonly, assign) GURL documentURL; @property(nonatomic, readonly, assign) GURL documentURL;
// Returns the js injector from self.delegate. // Returns the js injector from self.delegate.
@property(nonatomic, readonly, weak) CRWJSInjector* JSInjector; @property(nonatomic, readonly, weak) CRWJSInjector* JSInjector;
// Will handle highlighting text fragments on the page when necessary.
@property(nonatomic, strong) CRWTextFragmentsHandler* textFragmentsHandler;
@end @end
...@@ -133,6 +135,9 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation( ...@@ -133,6 +135,9 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation(
kMaxCertErrorsCount); kMaxCertErrorsCount);
_delegate = delegate; _delegate = delegate;
_textFragmentsHandler =
[[CRWTextFragmentsHandler alloc] initWithDelegate:_delegate];
} }
return self; return self;
} }
...@@ -1165,9 +1170,9 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation( ...@@ -1165,9 +1170,9 @@ void ReportOutOfSyncURLInDidStartProvisionalNavigation(
} }
} }
if (context && web::AreTextFragmentsAllowed(context)) { [self.textFragmentsHandler
web::HandleTextFragments(self.webStateImpl); processTextFragmentsWithContext:context
} referrer:self.currentReferrer];
[self.navigationStates setState:web::WKNavigationState::FINISHED [self.navigationStates setState:web::WKNavigationState::FINISHED
forNavigation:navigation]; forNavigation:navigation];
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#ifndef IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_ #ifndef IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
#define IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_ #define IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
#include "base/values.h" #include "base/values.h"
...@@ -11,26 +11,11 @@ class GURL; ...@@ -11,26 +11,11 @@ class GURL;
namespace web { namespace web {
class NavigationContext;
class WebState;
// This file contains helper functions relating to Text Fragments, which are // This file contains helper functions relating to Text Fragments, which are
// appended to the reference fragment in the URL and instruct the user agent // 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. // 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/ // 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 // Checks the fragment portion of the URL for Text Fragments. Returns zero or
// more dictionaries containing the parsed parameters used by the fragment- // more dictionaries containing the parsed parameters used by the fragment-
// finding algorithm, as defined in the spec. // finding algorithm, as defined in the spec.
...@@ -44,7 +29,6 @@ std::vector<std::string> ExtractTextFragments(std::string ref_string); ...@@ -44,7 +29,6 @@ std::vector<std::string> ExtractTextFragments(std::string ref_string);
// fragment is malformed. // fragment is malformed.
base::Value TextFragmentToValue(std::string fragment); base::Value TextFragmentToValue(std::string fragment);
} // namespace internal
} // namespace web } // namespace web
#endif // IOS_WEB_NAVIGATION_TEXT_FRAGMENT_UTILS_H_ #endif // IOS_WEB_NAVIGATION_TEXT_FRAGMENTS_UTILS_H_
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import "ios/web/navigation/text_fragment_utils.h" #import "ios/web/navigation/text_fragments_utils.h"
#include <cstring.h> #include <cstring.h>
...@@ -32,40 +32,6 @@ base::Value DecodeStringToValue(const std::string& str) { ...@@ -32,40 +32,6 @@ base::Value DecodeStringToValue(const std::string& str) {
namespace web { 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) { base::Value ParseTextFragments(const GURL& url) {
if (!url.has_ref()) if (!url.has_ref())
return {}; return {};
...@@ -171,5 +137,4 @@ base::Value TextFragmentToValue(std::string fragment) { ...@@ -171,5 +137,4 @@ base::Value TextFragmentToValue(std::string fragment) {
return dict; return dict;
} }
} // namespace internal
} // namespace web } // namespace web
...@@ -2,17 +2,11 @@ ...@@ -2,17 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import "ios/web/navigation/text_fragment_utils.h" #import "ios/web/navigation/text_fragments_utils.h"
#include <memory> #import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#include "base/test/scoped_feature_list.h" #import "url/gurl.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) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
...@@ -31,119 +25,84 @@ const char kSuffixKey[] = "suffix"; ...@@ -31,119 +25,84 @@ const char kSuffixKey[] = "suffix";
namespace web { namespace web {
typedef PlatformTest TextFragmentUtilsTest; typedef PlatformTest TextFragmentsUtilsTest;
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) { TEST_F(TextFragmentsUtilsTest, ParseTextFragments) {
GURL url_with_fragment( GURL url_with_fragment(
"https://www.example.com/#idFrag:~:text=text%201&text=text%202"); "https://www.example.com/#idFrag:~:text=text%201&text=text%202");
base::Value result = internal::ParseTextFragments(url_with_fragment); base::Value result = ParseTextFragments(url_with_fragment);
ASSERT_EQ(2u, result.GetList().size()); ASSERT_EQ(2u, result.GetList().size());
EXPECT_EQ("text 1", result.GetList()[0].FindKey(kTextStartKey)->GetString()); EXPECT_EQ("text 1", result.GetList()[0].FindKey(kTextStartKey)->GetString());
EXPECT_EQ("text 2", result.GetList()[1].FindKey(kTextStartKey)->GetString()); EXPECT_EQ("text 2", result.GetList()[1].FindKey(kTextStartKey)->GetString());
GURL url_no_fragment("www.example.com"); GURL url_no_fragment("www.example.com");
base::Value empty_result = internal::ParseTextFragments(url_no_fragment); base::Value empty_result = ParseTextFragments(url_no_fragment);
EXPECT_TRUE(empty_result.is_none()); EXPECT_TRUE(empty_result.is_none());
} }
TEST_F(TextFragmentUtilsTest, ExtractTextFragments) { TEST_F(TextFragmentsUtilsTest, ExtractTextFragments) {
std::vector<std::string> expected = {"test1", "test2", "test3"}; std::vector<std::string> expected = {"test1", "test2", "test3"};
// Ensure presence/absence of a trailing & doesn't break anything // Ensure presence/absence of a trailing & doesn't break anything
EXPECT_EQ(expected, internal::ExtractTextFragments( EXPECT_EQ(expected,
"#id:~:text=test1&text=test2&text=test3")); ExtractTextFragments("#id:~:text=test1&text=test2&text=test3"));
EXPECT_EQ(expected, internal::ExtractTextFragments( EXPECT_EQ(expected,
"#id:~:text=test1&text=test2&text=test3&")); ExtractTextFragments("#id:~:text=test1&text=test2&text=test3&"));
// Test that empty tokens (&& or &text=&) are discarded // Test that empty tokens (&& or &text=&) are discarded
EXPECT_EQ(expected, internal::ExtractTextFragments( EXPECT_EQ(expected, ExtractTextFragments(
"#id:~:text=test1&&text=test2&text=&text=test3")); "#id:~:text=test1&&text=test2&text=&text=test3"));
expected = {}; expected.clear();
EXPECT_EQ(expected, EXPECT_EQ(expected, ExtractTextFragments("#idButNoTextFragmentsHere"));
internal::ExtractTextFragments("#idButNoTextFragmentsHere")); EXPECT_EQ(expected, ExtractTextFragments(""));
EXPECT_EQ(expected, internal::ExtractTextFragments(""));
} }
TEST_F(TextFragmentUtilsTest, TextFragmentToValue) { TEST_F(TextFragmentsUtilsTest, TextFragmentToValue) {
// Success cases // Success cases
std::string fragment = "start"; std::string fragment = "start";
base::Value result = internal::TextFragmentToValue(fragment); base::Value result = TextFragmentToValue(fragment);
EXPECT_FALSE(result.FindKey(kPrefixKey)); EXPECT_FALSE(result.FindKey(kPrefixKey));
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_FALSE(result.FindKey(kTextEndKey)); EXPECT_FALSE(result.FindKey(kTextEndKey));
EXPECT_FALSE(result.FindKey(kSuffixKey)); EXPECT_FALSE(result.FindKey(kSuffixKey));
fragment = "start,end"; fragment = "start,end";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_FALSE(result.FindKey(kPrefixKey)); EXPECT_FALSE(result.FindKey(kPrefixKey));
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString()); EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
EXPECT_FALSE(result.FindKey(kSuffixKey)); EXPECT_FALSE(result.FindKey(kSuffixKey));
fragment = "prefix-,start"; fragment = "prefix-,start";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString()); EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_FALSE(result.FindKey(kTextEndKey)); EXPECT_FALSE(result.FindKey(kTextEndKey));
EXPECT_FALSE(result.FindKey(kSuffixKey)); EXPECT_FALSE(result.FindKey(kSuffixKey));
fragment = "start,-suffix"; fragment = "start,-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_FALSE(result.FindKey(kPrefixKey)); EXPECT_FALSE(result.FindKey(kPrefixKey));
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_FALSE(result.FindKey(kTextEndKey)); EXPECT_FALSE(result.FindKey(kTextEndKey));
EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString()); EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
fragment = "prefix-,start,end"; fragment = "prefix-,start,end";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString()); EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString()); EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
EXPECT_FALSE(result.FindKey(kSuffixKey)); EXPECT_FALSE(result.FindKey(kSuffixKey));
fragment = "start,end,-suffix"; fragment = "start,end,-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_FALSE(result.FindKey(kPrefixKey)); EXPECT_FALSE(result.FindKey(kPrefixKey));
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString()); EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString()); EXPECT_EQ("suffix", result.FindKey(kSuffixKey)->GetString());
fragment = "prefix-,start,end,-suffix"; fragment = "prefix-,start,end,-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString()); EXPECT_EQ("prefix", result.FindKey(kPrefixKey)->GetString());
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString()); EXPECT_EQ("end", result.FindKey(kTextEndKey)->GetString());
...@@ -151,7 +110,7 @@ TEST_F(TextFragmentUtilsTest, TextFragmentToValue) { ...@@ -151,7 +110,7 @@ TEST_F(TextFragmentUtilsTest, TextFragmentToValue) {
// Trailing comma doesn't break otherwise valid fragment // Trailing comma doesn't break otherwise valid fragment
fragment = "start,"; fragment = "start,";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_FALSE(result.FindKey(kPrefixKey)); EXPECT_FALSE(result.FindKey(kPrefixKey));
EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString()); EXPECT_EQ("start", result.FindKey(kTextStartKey)->GetString());
EXPECT_FALSE(result.FindKey(kTextEndKey)); EXPECT_FALSE(result.FindKey(kTextEndKey));
...@@ -159,31 +118,31 @@ TEST_F(TextFragmentUtilsTest, TextFragmentToValue) { ...@@ -159,31 +118,31 @@ TEST_F(TextFragmentUtilsTest, TextFragmentToValue) {
// Failure Cases // Failure Cases
fragment = ""; fragment = "";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "some,really-,malformed,-thing,with,too,many,commas"; fragment = "some,really-,malformed,-thing,with,too,many,commas";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "prefix-,-suffix"; fragment = "prefix-,-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "start,prefix-,-suffix"; fragment = "start,prefix-,-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "prefix-,-suffix,start"; fragment = "prefix-,-suffix,start";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "prefix-"; fragment = "prefix-";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
fragment = "-suffix"; fragment = "-suffix";
result = internal::TextFragmentToValue(fragment); result = TextFragmentToValue(fragment);
EXPECT_EQ(base::Value::Type::NONE, result.type()); EXPECT_EQ(base::Value::Type::NONE, result.type());
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment