[go: nahoru, domu]

blob: e5078aaa8e16e6329d8804c4b7ee1c1635af399d [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
#import "base/memory/ptr_util.h"
#import "base/memory/raw_ptr.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "ios/web/annotations/annotations_java_script_feature.h"
#import "ios/web/common/annotations_utils.h"
#import "ios/web/common/features.h"
#import "ios/web/js_messaging/java_script_feature_manager.h"
#import "ios/web/js_messaging/web_frame_impl.h"
#import "ios/web/public/annotations/annotations_text_manager.h"
#import "ios/web/public/annotations/annotations_text_observer.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
using base::test::ios::kWaitForActionTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
namespace web {
namespace {
const char kScriptName[] = "annotations_test";
// Feature to include test ts code only.
class AnnotationsTestJavaScriptFeature : public JavaScriptFeature {
public:
AnnotationsTestJavaScriptFeature()
: JavaScriptFeature(
ContentWorld::kIsolatedWorld,
{FeatureScript::CreateWithFilename(
kScriptName,
FeatureScript::InjectionTime::kDocumentStart,
FeatureScript::TargetFrames::kMainFrame,
FeatureScript::ReinjectionBehavior::kInjectOncePerWindow)}) {}
AnnotationsTestJavaScriptFeature(const AnnotationsTestJavaScriptFeature&) =
delete;
AnnotationsTestJavaScriptFeature& operator=(
const AnnotationsTestJavaScriptFeature&) = delete;
};
// Class used to observe AnnotationTextManager interactions with an observer.
class TestAnnotationTextObserver : public AnnotationsTextObserver {
public:
TestAnnotationTextObserver() : successes_(0), annotations_(0), clicks_(0) {}
TestAnnotationTextObserver(const TestAnnotationTextObserver&) = delete;
TestAnnotationTextObserver& operator=(const TestAnnotationTextObserver&) =
delete;
void OnTextExtracted(WebState* web_state,
const std::string& text,
int seq_id,
const base::Value::Dict& metadata) override {
extracted_text_ = text;
seq_id_ = seq_id;
metadata_ = metadata.Clone();
}
void OnDecorated(WebState* web_state,
int annotations,
int successes,
int failures,
const base::Value::List& cancelled) override {
annotations_ = annotations;
successes_ = successes;
failures_ = failures;
}
void OnClick(WebState* web_state,
const std::string& text,
CGRect rect,
const std::string& data) override {
clicks_++;
click_data_ = data;
}
const std::string& extracted_text() const { return extracted_text_; }
int successes() const { return successes_; }
int failures() const { return failures_; }
int annotations() const { return annotations_; }
int clicks() const { return clicks_; }
int seq_id() const { return seq_id_; }
const base::Value::Dict& metadata() const { return metadata_; }
const std::string& click_data() const { return click_data_; }
void SetAnnotations(int count) { annotations_ = count; }
private:
std::string extracted_text_, click_data_;
int successes_, failures_, annotations_, clicks_, seq_id_;
base::Value::Dict metadata_;
};
} // namespace
// Test fixture for WebStateDelegate::FaviconUrlUpdated and integration tests.
class AnnotationTextManagerTest : public web::WebTestWithWebState {
public:
AnnotationTextManagerTest() = default;
AnnotationTextManagerTest(const AnnotationTextManagerTest&) = delete;
AnnotationTextManagerTest& operator=(const AnnotationTextManagerTest&) =
delete;
protected:
void SetUp() override {
WebTestWithWebState::SetUp();
AnnotationsTextManager::CreateForWebState(web_state());
auto* manager = AnnotationsTextManager::FromWebState(web_state());
manager->AddObserver(&observer_);
manager->SetSupportedTypes(NSTextCheckingAllTypes);
WKWebViewConfigurationProvider& configuration_provider =
WKWebViewConfigurationProvider::FromBrowserState(GetBrowserState());
// Force the creation of the content worlds.
configuration_provider.GetWebViewConfiguration();
content_world_ =
JavaScriptFeatureManager::FromBrowserState(GetBrowserState())
->GetContentWorldForFeature(
AnnotationsJavaScriptFeature::GetInstance());
// Inject ts test helpers functions.
content_world_->AddFeature(&js_test_feature_);
}
void TearDown() override {
auto* manager = AnnotationsTextManager::FromWebState(web_state());
manager->RemoveObserver(&observer_);
WebTestWithWebState::TearDown();
}
bool WaitForWebFramesCount(unsigned long web_frames_count) {
return WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return AllWebFrames().size() == web_frames_count;
});
}
// Returns all web frames for `web_state()`.
std::set<WebFrameImpl*> AllWebFrames() {
std::set<WebFrameImpl*> frames;
for (WebFrame* frame :
web_state()->GetPageWorldWebFramesManager()->GetAllWebFrames()) {
frames.insert(static_cast<WebFrameImpl*>(frame));
}
return frames;
}
// Returns main frame for `web_state_`.
WebFrameInternal* MainWebFrame() {
WebFrame* main_frame =
web_state()->GetPageWorldWebFramesManager()->GetMainWebFrame();
return main_frame->GetWebFrameInternal();
}
// Loads given `html` and waits until text is extracted.
void LoadHtmlAndExtractText(const std::string& html) {
int seq_id = observer()->seq_id();
ASSERT_TRUE(LoadHtml(html));
ASSERT_TRUE(WaitForWebFramesCount(1));
// Wait for text extracted, background parsing and decoration.
// Make timeout 3 times the regular action timeout to reduce flakiness.
EXPECT_TRUE(WaitUntilConditionOrTimeout(3 * kWaitForActionTimeout, ^{
return observer()->seq_id() > seq_id;
}));
}
// Creates and applies annotations based on `source` text and all matching
// `items`. `items` is a dictionary when the key is the annotation type to
// apply to the its values.
void CreateAndApplyAnnotationsWithTypes(
NSString* source,
NSDictionary<NSString*, NSArray<NSString*>*>* items,
int seq_id) {
// Create annotation.
base::Value::List annotations;
for (NSString* type in items) {
for (NSString* item in items[type]) {
NSRange range = [source rangeOfString:item];
web::TextAnnotation annotation =
web::ConvertMatchToAnnotation(source, range, nil, type);
annotation.first.Set(
"data", base::SysNSStringToUTF8(
[NSString stringWithFormat:@"%@-%@", type, item]));
annotations.Append(base::Value(std::move(annotation.first)));
}
}
auto* manager = AnnotationsTextManager::FromWebState(web_state());
base::Value value = base::Value(std::move(annotations));
manager->DecorateAnnotations(web_state(), value, seq_id);
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
return observer()->annotations() > 0;
}));
}
// Creates and applies annotations based on `source` text and all matching
// `items` with type "type".
void CreateAndApplyAnnotations(NSString* source,
NSArray<NSString*>* items,
int seq_id) {
CreateAndApplyAnnotationsWithTypes(source, @{@"type" : items}, seq_id);
}
// Verifies the now state of html text and tags of the document. Tags have no
// properties.
void CheckHtml(const std::string& html) {
const base::TimeDelta kCallJavascriptFunctionTimeout =
kWaitForJSCompletionTimeout;
__block bool message_received = false;
base::Value::List params;
params.Append(1000);
MainWebFrame()->CallJavaScriptFunctionInContentWorld(
"annotationsTest.getPageTaggedText", params, content_world_,
base::BindOnce(^(const base::Value* result) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->is_string());
EXPECT_EQ(html, result->GetString());
message_received = true;
}),
kCallJavascriptFunctionTimeout);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received;
}));
}
// Simulates clicking on annotation at given `index`.
void ClickAnnotation(int index) {
const base::TimeDelta kCallJavascriptFunctionTimeout =
kWaitForJSCompletionTimeout;
__block bool message_received = false;
base::Value::List params;
params.Append(index);
MainWebFrame()->CallJavaScriptFunctionInContentWorld(
"annotationsTest.clickAnnotation", params, content_world_,
base::BindOnce(^(const base::Value* result) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->is_bool());
EXPECT_TRUE(result->GetBool());
message_received = true;
}),
kCallJavascriptFunctionTimeout);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received;
}));
}
// Updates count of annotations in observer.
void CountAnnotation() {
const base::TimeDelta kCallJavascriptFunctionTimeout =
kWaitForJSCompletionTimeout;
__block bool message_received = false;
base::Value::List params;
MainWebFrame()->CallJavaScriptFunctionInContentWorld(
"annotationsTest.countAnnotations", params, content_world_,
base::BindOnce(^(const base::Value* result) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->is_double());
observer_.SetAnnotations(result->GetDouble());
message_received = true;
}),
kCallJavascriptFunctionTimeout);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return message_received;
}));
}
TestAnnotationTextObserver* observer() { return &observer_; }
base::test::ScopedFeatureList feature_;
raw_ptr<JavaScriptContentWorld> content_world_;
TestAnnotationTextObserver observer_;
AnnotationsTestJavaScriptFeature js_test_feature_;
};
// Tests page text extraction.
// Covers: PageLoaded, OnTextExtracted, StartExtractingText.
TEST_F(AnnotationTextManagerTest, ExtractText) {
LoadHtmlAndExtractText("<html><body>"
"<p>You'll find it on</p>"
"<p>Castro Street, <span>Mountain View</span>, CA</p>"
"<p>Enjoy</p>"
"</body></html>");
EXPECT_EQ("You'll find it on"
"\nCastro Street, Mountain View, CA"
"\nEnjoy",
observer()->extracted_text());
}
// Tests no page text extraction if there is no supported type.
TEST_F(AnnotationTextManagerTest, ExtractNoText) {
auto* manager = AnnotationsTextManager::FromWebState(web_state());
manager->SetSupportedTypes(0);
int seq_id = observer()->seq_id();
ASSERT_TRUE(LoadHtml("<html><body>"
"<p>You'll find it on</p>"
"<p>Castro Street, <span>Mountain View</span>, CA</p>"
"<p>Enjoy</p>"
"</body></html>"));
ASSERT_TRUE(WaitForWebFramesCount(1));
EXPECT_FALSE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
return observer()->seq_id() > seq_id;
}));
EXPECT_EQ("", observer()->extracted_text());
}
TEST_F(AnnotationTextManagerTest, CheckMetadata) {
LoadHtmlAndExtractText("<html lang=\"fr\">"
"<head>"
"<meta http-equiv=\"content-language\" content=\"fr\">"
"<meta name=\"chrome\" content=\"nointentdetection\"/>"
"<meta name=\"google\" content=\"notranslate\"/>"
"</head>"
"<body>"
"<p>You'll find it on</p>"
"<p>Castro Street, <span>Mountain View</span>, CA</p>"
"<p>Enjoy</p>"
"</body></html>");
std::string fr = "fr";
EXPECT_TRUE(observer()->metadata().FindBool("hasNoIntentDetection").value());
EXPECT_TRUE(observer()->metadata().FindBool("hasNoTranslate").value());
EXPECT_EQ(fr, *observer()->metadata().FindString("htmlLang"));
EXPECT_EQ(fr, *observer()->metadata().FindString("httpContentLanguage"));
}
TEST_F(AnnotationTextManagerTest, CheckNoMetadata) {
LoadHtmlAndExtractText("<html>"
"<head>"
"</head>"
"<body>"
"<p>You'll find it on</p>"
"<p>Castro Street, <span>Mountain View</span>, CA</p>"
"<p>Enjoy</p>"
"</body></html>");
std::string empty = "";
EXPECT_FALSE(observer()->metadata().FindBool("hasNoIntentDetection").value());
EXPECT_FALSE(observer()->metadata().FindBool("hasNoTranslate").value());
EXPECT_EQ(empty, *observer()->metadata().FindString("htmlLang"));
EXPECT_EQ(empty, *observer()->metadata().FindString("httpContentLanguage"));
}
// Tests page decoration when page doesn't change.
// Covers: DecorateAnnotations, ConvertMatchToAnnotation.
TEST_F(AnnotationTextManagerTest, DecorateText) {
LoadHtmlAndExtractText("<html><body>"
"<p>text</p>"
"<p>annotation</p>"
"<p>text</p>"
"</body></html>");
std::string text = "text"
"\nannotation"
"\ntext";
EXPECT_EQ(text, observer()->extracted_text());
// Create annotation.
NSString* source = base::SysUTF8ToNSString(text);
CreateAndApplyAnnotations(source, @[ @"annotation" ], observer() -> seq_id());
EXPECT_EQ(observer()->successes(), 1);
EXPECT_EQ(observer()->annotations(), 1);
// Check the resulting html is annotating at the right place.
CheckHtml("<html><body>"
"<p>text</p>"
"<p><chrome_annotation>annotation</chrome_annotation></p>"
"<p>text</p>"
"</body></html>");
}
// Tests on no-decoration tags.
TEST_F(AnnotationTextManagerTest, NoDecorateText) {
LoadHtmlAndExtractText("<html><body>"
"<p>text</p>"
"<a>annotation1</a>"
"<input type=\"radio\">"
"<label>annotation2</label>"
"<p>text</p>"
"</body></html>");
std::string text = "text"
"\ntext";
EXPECT_EQ(text, observer()->extracted_text());
}
// Tests different annotation cases, including tags boundaries.
// Covers: RemoveDecorations
TEST_F(AnnotationTextManagerTest, DecorateTextCrossingElements) {
std::string html = "<html><body>"
"<p>abc</p>"
"<p>def</p>"
"<p>ghi</p>"
"<p>jkl</p>"
"<p>mno</p>"
"</body></html>";
LoadHtmlAndExtractText(html);
NSString* source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotations(source, @[ @"a", @"c\nd", @"f\nghi\nj", @"l\nmno" ],
observer() -> seq_id());
// Check the resulting html is annotating at the right place.
CheckHtml("<html><body>"
"<p><chrome_annotation>a</chrome_annotation>b<chrome_annotation>c</"
"chrome_annotation></p>"
"<p><chrome_annotation>d</chrome_annotation>e<chrome_annotation>f</"
"chrome_annotation></p>"
"<p><chrome_annotation>ghi</chrome_annotation></p>"
"<p><chrome_annotation>j</chrome_annotation>k<chrome_annotation>l</"
"chrome_annotation></p>"
"<p><chrome_annotation>mno</chrome_annotation></p>"
"</body></html>");
// Make sure it's back to the original.
auto* manager = AnnotationsTextManager::FromWebState(web_state());
manager->RemoveDecorations();
CheckHtml(html);
}
// Tests annotation cases with line breaks, including tags boundaries.
// Covers: DecorateAnnotations, RemoveDecorations
TEST_F(AnnotationTextManagerTest, DecorateTextBreakElements) {
std::string html = "<html><body>"
"<p>abc<br>\ndef</p>"
"</body></html>";
LoadHtmlAndExtractText(html);
CheckHtml(html);
NSString* source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotations(source, @[ @"abc\n\ndef" ], observer() -> seq_id());
// Check the resulting html is annotating at the right place.
CheckHtml("<html><body>"
"<p><chrome_annotation>abc</chrome_annotation><br>"
"<chrome_annotation>\ndef</chrome_annotation></p>"
"</body></html>");
// Make sure it's back to the original.
auto* manager = AnnotationsTextManager::FromWebState(web_state());
manager->RemoveDecorations();
CheckHtml(html);
}
// Tests on click handler.
// Covers: OnClick.
TEST_F(AnnotationTextManagerTest, ClickAnnotation) {
LoadHtmlAndExtractText("<html><body>"
"<p>text</p>"
"<p>annotation</p>"
"<p>text</p>"
"</body></html>");
NSString* source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotations(source, @[ @"annotation" ], observer() -> seq_id());
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 1;
}));
}
// Tests removing annotation of one type
TEST_F(AnnotationTextManagerTest, RemoveDecorationTypeTest) {
std::string html = "<html><body>"
"<p>abc def</p>"
"<p>zzzzz ghi zzzzz</p>"
"<p>zzzzz klm zzzzz</p>"
"</body></html>";
LoadHtmlAndExtractText(html);
CheckHtml(html);
auto* manager = AnnotationsTextManager::FromWebState(web_state());
NSString* source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotationsWithTypes(
source,
@{@"type1" : @[ @"abc", @"ghi" ],
@"type2" : @[ @"def", @"klm" ]},
observer()->seq_id());
// Check the resulting html is annotating at the right place.
CheckHtml("<html><body>"
"<p><chrome_annotation>abc</chrome_annotation> "
"<chrome_annotation>def</chrome_annotation></p>"
"<p>zzzzz <chrome_annotation>ghi</chrome_annotation> zzzzz</p>"
"<p>zzzzz <chrome_annotation>klm</chrome_annotation> zzzzz</p>"
"</body></html>");
CountAnnotation();
ASSERT_EQ(observer()->annotations(), 4);
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 1;
}));
ClickAnnotation(1);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 2;
}));
ClickAnnotation(2);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 3;
}));
ClickAnnotation(3);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 4;
}));
manager->RemoveDecorationsWithType("type1");
// Check the resulting html is annotating at the right place.
CheckHtml("<html><body>"
"<p>abc <chrome_annotation>def</chrome_annotation></p>"
"<p>zzzzz ghi zzzzz</p>"
"<p>zzzzz <chrome_annotation>klm</chrome_annotation> zzzzz</p>"
"</body></html>");
CountAnnotation();
ASSERT_EQ(observer()->annotations(), 2);
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 5;
}));
ClickAnnotation(1);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 6;
}));
// Make sure it's back to the original.
manager->RemoveDecorations();
CheckHtml(html);
}
// Tests on (simulated) navigation in web state.
TEST_F(AnnotationTextManagerTest, NavigationClearsAnnotation) {
std::string text1 = "<html><body>"
"<p>text</p>"
"<p>annotation</p>"
"<p>text</p>"
"</body></html>";
LoadHtmlAndExtractText(text1);
NSString* source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotationsWithTypes(
source,
@{@"type1" : @[ @"annotation" ]}, observer()->seq_id());
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 1;
}));
ASSERT_TRUE(observer()->click_data() == "type1-annotation");
std::string text2 = "<html><body>"
"<p>bla</p>"
"<p>blurb</p>"
"<p>bla</p>"
"</body></html>";
LoadHtmlAndExtractText(text2);
source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotationsWithTypes(
source,
@{@"type2" : @[ @"blurb" ]}, observer()->seq_id());
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 2;
}));
ASSERT_TRUE(observer()->click_data() == "type2-blurb");
// Now navigate back to original text.
LoadHtmlAndExtractText(text1);
source = base::SysUTF8ToNSString(observer()->extracted_text());
CreateAndApplyAnnotationsWithTypes(
source,
@{@"type1" : @[ @"annotation" ]}, observer()->seq_id());
ClickAnnotation(0);
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
return observer()->clicks() == 3;
}));
ASSERT_TRUE(observer()->click_data() == "type1-annotation");
}
} // namespace web