blob: 52f3d5169923bb8ab2e7a4590eae09c52f8a860e [file] [log] [blame]
// Copyright 2014 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 <UIKit/UIKit.h>
#include <memory>
#include "base/ios/ios_util.h"
#include "base/json/json_reader.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_registry_simple.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/ui/contextual_search/contextual_search_controller.h"
#import "ios/chrome/browser/ui/contextual_search/js_contextual_search_manager.h"
#import "ios/chrome/browser/ui/contextual_search/touch_to_search_permissions_mediator+testing.h"
#import "ios/chrome/browser/web/chrome_web_test.h"
#import "ios/web/public/web_state/js/crw_js_injection_manager.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/web_state.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// Unit tests for the resources/contextualsearch.js JavaScript file.
struct ContextualSearchStruct {
std::string url;
std::string selectedText;
std::string surroundingText;
int offsetStart;
int offsetEnd;
};
@interface JsContextualSearchAdditionsManager : CRWJSInjectionManager
@end
@implementation JsContextualSearchAdditionsManager : CRWJSInjectionManager
- (NSString*)scriptPath {
return @"contextualsearch_unittest";
}
- (NSArray*)dependencies {
return @[ [JsContextualSearchManager class] ];
}
@end
namespace {
// HTML that contains script.
NSString* kHTMLSentenceWithScript =
@"<html><body>"
"This is <span id='taphere'>the</span> <script>function ignore() "
"{};</script>sentence to select."
"</body></html>";
// HTML that contains label to be tapped.
NSString* kHTMLSentenceWithLabel =
@"<html><body>"
"<label>Click <span id='taphere'>me</span> <input type='text'/></label>."
"A sentence to select."
"</body></html>";
// HTML that contains italic element.
NSString* kHTMLWithItalic =
@"<html><body>"
"This is <span id='taphere'>an</span> <i>italic</i> element."
"</body></html>";
// HTML that contains a table.
NSString* kHTMLWithTable =
@"<html><body>"
"<table><tr><td>Left <span id='taphere'>cell</span></td>"
"<td>right cell</td></tr></table>"
"</body></html>";
// HTML that will trigger an unrelated DOM mutation on tap.
NSString* kHTMLWithUnrelatedDOMMutation =
@"<html><body>"
"<span>This <span style='margin-left:50px' id='taphere'>is</span>"
" sentence</span> <span>with <span id='test' attr='before'>mutation</span>"
"</span>.</body></html>";
// HTML that will trigger a related DOM mutation on tap.
NSString* kHTMLWithRelatedDOMMutation =
@"<html><body>"
"This <span id='test' attr='before'>"
"<span style='margin-left:50px' id='taphere'>is</span></span>"
"a sentence with mutation."
"</body></html>";
// HTML that will trigger a related DOM mutation on tap.
NSString* kHTMLWithRelatedTextMutation =
@"<html><body>"
"This <span style='margin-left:50px' id='taphere'> text "
"<span id='test'>mutation is </span> inside </span>"
"a sentence with mutation."
"</body></html>";
// HTML that contain a div in the middle of a sentence.
NSString* kHTMLWithDiv =
@"<html><body>"
"This<div>is</div>a <span id='taphere'>sentence</span>."
"</body></html>";
// HTML that contain a div in the middle of a sentence.
NSString* kHTMLWithSpaceDiv =
@"<html><body>"
"This <div>is</div>a <span id='taphere'>sentence</span>."
"</body></html>";
// HTML that contains prevent default.
NSString* kHTMLWithPreventDefault = @"<html><body>"
"<div id='interceptor'>"
"<span id='taphere'>is</span>"
"</div></body></html>";
NSString* kStringWith50Chars =
@"Sentence $INDEX containing exactly 50 characters. ";
class ContextualSearchJsTest : public ChromeWebTest {
public:
// Loads the given HTML, then loads the |contextualSearch| JavaScript.
void LoadHtml(NSString* html) {
ChromeWebTest::LoadHtml(html);
[jsUnittestsAdditions_ inject];
}
bool GetContextFromId(NSString* elementID,
ContextualSearchStruct* searchContext) {
id javaScriptResult = ExecuteJavaScript([NSString
stringWithFormat:@"document.getElementById('%@').scrollIntoView();"
"__gCrWeb.contextualSearch.tapOnElement('%@');",
elementID, elementID]);
if (!javaScriptResult)
return false;
const std::string json = base::SysNSStringToUTF8(javaScriptResult);
std::unique_ptr<base::Value> parsedResult(
base::JSONReader::Read(json, false));
if (!parsedResult.get() ||
!parsedResult->IsType(base::Value::Type::DICTIONARY)) {
return false;
}
base::DictionaryValue* resultDict =
static_cast<base::DictionaryValue*>(parsedResult.get());
const base::DictionaryValue* context = nullptr;
if (!resultDict->GetDictionary("context", &context)) {
return false;
}
std::string error;
context->GetString("error", &error);
if (!error.empty()) {
LOG(ERROR) << "GetContext error: " << error;
return false;
}
std::string url, selectedText;
context->GetString("url", &searchContext->url);
context->GetString("selectedText", &searchContext->selectedText);
context->GetString("surroundingText", &searchContext->surroundingText);
context->GetInteger("offsetStart", &searchContext->offsetStart);
context->GetInteger("offsetEnd", &searchContext->offsetEnd);
return true;
}
id expandHighlight(int start, int end) {
return ExecuteJavaScript([NSString
stringWithFormat:@"__gCrWeb.contextualSearch.expandHighlight(%d, %d);"
"__gCrWeb.contextualSearch.retrieveHighlighted();",
start, end]);
}
id highlight() {
return ExecuteJavaScript(
@"__gCrWeb.contextualSearch.setHighlighting(true);"
"__gCrWeb.contextualSearch.retrieveHighlighted();");
}
NSInteger GetMutatedNodeCount() {
id output = ExecuteJavaScript(
@"__gCrWeb.contextualSearch.getMutatedElementCount();");
return [output integerValue];
}
void CheckContextOffsets(const ContextualSearchStruct& searchContext,
const std::string& surroundingText) {
EXPECT_EQ(searchContext.surroundingText, surroundingText);
EXPECT_EQ(searchContext.selectedText,
searchContext.surroundingText.substr(
searchContext.offsetStart,
searchContext.offsetEnd - searchContext.offsetStart));
}
NSString* BuildLongTestString(int startIndex,
int endIndex,
int tapOn,
bool newLine) {
NSString* longString = @"";
for (int i = startIndex; i < endIndex; i++) {
NSString* index = [NSString stringWithFormat:@"%06d", i];
if (i == tapOn) {
index =
[NSString stringWithFormat:@"<span id='taphere'>%@</span>", index];
}
NSString* sentence =
[kStringWith50Chars stringByReplacingOccurrencesOfString:@"$INDEX"
withString:index];
longString = [longString stringByAppendingString:sentence];
if (newLine) {
longString = [longString stringByAppendingString:@"<br/>"];
}
}
return longString;
}
void SetUp() override {
ChromeWebTest::SetUp();
mockDelegate_ = [OCMockObject
niceMockForProtocol:@protocol(ContextualSearchControllerDelegate)];
jsUnittestsAdditions_ = static_cast<JsContextualSearchAdditionsManager*>(
[web_state()->GetJSInjectionReceiver()
instanceOfClass:[JsContextualSearchAdditionsManager class]]);
TestChromeBrowserState::Builder test_cbs_builder;
chrome_browser_state_ = test_cbs_builder.Build();
controller_ = [[ContextualSearchController alloc]
initWithBrowserState:chrome_browser_state_.get()
delegate:mockDelegate_];
[controller_
setPermissions:[[MockTouchToSearchPermissionsMediator alloc]
initWithBrowserState:chrome_browser_state_.get()]];
[controller_ setWebState:web_state()];
[controller_ enableContextualSearch:YES];
}
void TearDown() override {
[controller_ close];
// Need to tear down the controller so it deregisters its JS handlers
// before |webController_| is destroyed.
controller_ = nil;
ChromeWebTest::TearDown();
}
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
__unsafe_unretained JsContextualSearchAdditionsManager* jsUnittestsAdditions_;
ContextualSearchController* controller_;
id mockDelegate_;
id mockToolbarDelegate_;
};
// Test that ignored elements do not trigger CS when tapped.
TEST_F(ContextualSearchJsTest, TestIgnoreTapsOnElements) {
LoadHtml(kHTMLSentenceWithLabel);
ContextualSearchStruct searchContext;
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
// Test that ingnored element are not included in highlight or surrounding text.
TEST_F(ContextualSearchJsTest, TestIgnoreScript) {
LoadHtml(kHTMLSentenceWithScript);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, "This is the sentence to select.");
id highlighted = expandHighlight(0, searchContext.surroundingText.size());
EXPECT_NSEQ(highlighted, @"This is the sentence to select.");
};
// Test that all span element are correctly highlighted.
TEST_F(ContextualSearchJsTest, TestHighlightThroughSpan) {
LoadHtml(kHTMLWithItalic);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, "This is an italic element.");
NSString* highlighted =
expandHighlight(0, searchContext.surroundingText.size());
EXPECT_NSEQ(highlighted, @"This is an italic element.");
};
// Test that all block element are highlighted. Spaces separating element must
// be ignored.
TEST_F(ContextualSearchJsTest, TestHighlightThroughBlock) {
LoadHtml(kHTMLWithTable);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, "Left cell right cell");
NSString* highlighted =
expandHighlight(0, searchContext.surroundingText.size());
EXPECT_NSEQ(highlighted, @"Left cell right cell");
};
// Test that blocks add spaces if there are not arround it.
TEST_F(ContextualSearchJsTest, TestHighlightBlockAddsSpace) {
LoadHtml(kHTMLWithDiv);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, "This is a sentence.");
};
// Test that blocks don't add spaces if there are around it.
TEST_F(ContextualSearchJsTest, TestHighlightBlockDontAddsSpace) {
LoadHtml(kHTMLWithSpaceDiv);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, "This is a sentence.");
};
// Test that related DOM mutation cancels contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightRelatedDOMMutation) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('test').setAttribute('attr', 'after');");
ASSERT_EQ(1, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
// Test that unrelated DOM mutation does not cancel contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightIgnoreUnrelatedDOMMutation) {
LoadHtml(kHTMLWithUnrelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('test').setAttribute('attr', 'after');");
ASSERT_EQ(1, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that DOM mutation with same value does not cancel contextual search.
// Mutation should not mark a node as mutated.
TEST_F(ContextualSearchJsTest, TestHighlightIgnoreDOMMutationSameAttribute) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('test').setAttribute('attr', 'before');");
ASSERT_EQ(0, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that DOM mutation between different false values does not cancel
// contextual search. Mutation should not mark a node as mutated.
TEST_F(ContextualSearchJsTest, TestHighlightIgnoreDOMMutationBothFalse) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('test').setAttribute('non_attr', '');");
ASSERT_EQ(0, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that DOM mutation with same text does not cancel contextual search.
// Mutation should not mark a node as mutated.
TEST_F(ContextualSearchJsTest, TestHighlightIgnoreDOMMutationSameText) {
LoadHtml(kHTMLWithRelatedTextMutation);
ExecuteJavaScript(
@"document.getElementById('test').innerText = 'mutation is ';");
ASSERT_EQ(0, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that related text DOM mutation prevents contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightRelatedDOMMutationText) {
LoadHtml(kHTMLWithRelatedTextMutation);
ExecuteJavaScript(@"document.getElementById('test').innerText = 'mutated';");
ASSERT_EQ(1, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
// Test that unrelated text DOM mutation doesn't prevent contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightUnrelatedDOMMutationTextIgnored) {
LoadHtml(kHTMLWithUnrelatedDOMMutation);
ExecuteJavaScript(@"document.getElementById('test').innerText = 'mutated';");
ASSERT_EQ(1, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that two related DOM mutations prevent contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightTwoDOMMutations) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ASSERT_EQ(0, GetMutatedNodeCount());
ExecuteJavaScript(
@"document.getElementById('taphere').innerText = 'mutated';"
"document.getElementById('test').setAttribute('attr', 'after');");
ASSERT_EQ(2, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
// Test that two related DOM mutations with only one change prevent contextual
// search.
TEST_F(ContextualSearchJsTest, TestHighlightTwoDOMMutationOneChanging) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('taphere').innerText = 'mutated';"
"document.getElementById('test').setAttribute('attr', 'before');");
ASSERT_EQ(1, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
// Test that two DOM mutations with same value does not cancel contextual
// search.
TEST_F(ContextualSearchJsTest, TestHighlightTwoDOMMutationNoChange) {
LoadHtml(kHTMLWithRelatedDOMMutation);
ExecuteJavaScript(
@"document.getElementById('taphere').innerText = 'is';"
"document.getElementById('test').setAttribute('attr', 'before');");
ASSERT_EQ(0, GetMutatedNodeCount());
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
};
// Test that non bubbling event does not trigger contextual search.
TEST_F(ContextualSearchJsTest, TestHighlightIgnorePreventDefault) {
LoadHtml(kHTMLWithPreventDefault);
// Enable touch delay.
ExecuteJavaScript(
@"__gCrWeb.contextualSearch.setBodyTouchListenerDelay(200);");
ContextualSearchStruct searchContext;
ExecuteJavaScript(@"document.getElementById('interceptor')."
@"addEventListener('touchend', function(event) "
@"{event.preventDefault();return false;}, false);");
ASSERT_FALSE(GetContextFromId(@"taphere", &searchContext));
};
TEST_F(ContextualSearchJsTest, Test1500CharactersCenter) {
// String should extend 15 sentences to the left and right.
NSString* stringHTML = BuildLongTestString(0, 50, 25, true);
NSString* expectedHTML = BuildLongTestString(10, 41, -1, false);
LoadHtml(stringHTML);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, base::SysNSStringToUTF8(expectedHTML));
};
TEST_F(ContextualSearchJsTest, Test1500CharactersRight) {
// There is not enough chars on the left, so string should extend on the
// right.
NSString* stringHTML = BuildLongTestString(0, 50, 5, true);
NSString* expectedHTML = BuildLongTestString(0, 30, -1, false);
LoadHtml(
[NSString stringWithFormat:@"<html><body>%@</body></html>", stringHTML]);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, base::SysNSStringToUTF8(expectedHTML));
};
TEST_F(ContextualSearchJsTest, Test1500CharactersLeft) {
// There is not enough chars on the right, so string should extend on the
// left.
NSString* stringHTML = BuildLongTestString(0, 50, 45, true);
NSString* expectedHTML = BuildLongTestString(20, 50, -1, false);
LoadHtml(
[NSString stringWithFormat:@"<html><body>%@</body></html>", stringHTML]);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, base::SysNSStringToUTF8(expectedHTML));
};
TEST_F(ContextualSearchJsTest, Test1500CharactersToShort) {
// String is too short so the whole string should be in the context.
NSString* stringHTML = BuildLongTestString(0, 10, 5, true);
NSString* expectedHTML = BuildLongTestString(0, 10, -1, false);
// LoadHtml will trim the last space.
LoadHtml(
[NSString stringWithFormat:@"<html><body>%@</body></html>", stringHTML]);
ContextualSearchStruct searchContext;
ASSERT_TRUE(GetContextFromId(@"taphere", &searchContext));
CheckContextOffsets(searchContext, base::SysNSStringToUTF8(expectedHTML));
};
} // namespace