blob: ed911754ed060ed5788bbc6d2a5c3ca255564061 [file] [log] [blame]
// Copyright 2013 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.
#include <string.h>
#include <memory>
#include <vector>
#include "base/bind.h"
#import "base/test/ios/wait_util.h"
#include "base/values.h"
#import "components/translate/ios/browser/language_detection_controller.h"
#import "ios/chrome/browser/web/chrome_web_client.h"
#import "ios/chrome/browser/web/chrome_web_test.h"
#include "ios/chrome/common/string_util.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/web_state.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const char kExpectedLanguage[] = "Foo";
// Returns an NSString filled with the char 'a' of length |length|.
NSString* GetLongString(NSUInteger length) {
NSMutableData* data = [[NSMutableData alloc] initWithLength:length];
memset([data mutableBytes], 'a', length);
NSString* long_string = [[NSString alloc] initWithData:data
encoding:NSASCIIStringEncoding];
return long_string;
}
} // namespace
// Test fixture to test language detection.
class JsLanguageDetectionManagerTest : public ChromeWebTest {
protected:
JsLanguageDetectionManagerTest()
: ChromeWebTest(std::make_unique<ChromeWebClient>()) {}
~JsLanguageDetectionManagerTest() override {}
// Injects JS, waits for the completion handler and verifies if the result
// was what was expected.
void InjectJsAndVerify(NSString* js, id expected_result) {
EXPECT_NSEQ(expected_result, ExecuteJavaScript(js));
}
// Injects JS, and spins the run loop until |condition| block returns true
void InjectJSAndWaitUntilCondition(NSString* js, ConditionBlock condition) {
ExecuteJavaScript(js);
base::test::ios::WaitUntilCondition(^bool() {
return condition();
});
}
// Verifies if translation was allowed or not on the page based on
// |expected_value|.
void ExpectTranslationAllowed(BOOL expected_value) {
InjectJsAndVerify(@"__gCrWeb.languageDetection.translationAllowed();",
@(expected_value));
}
// Verifies if |lang| attribute of the HTML tag is the |expected_html_lang|,
void ExpectHtmlLang(NSString* expected_html_lang) {
InjectJsAndVerify(@"document.documentElement.lang;", expected_html_lang);
}
// Verifies if the value of the |Content-Language| meta tag is the same as
// |expected_http_content_language|.
void ExpectHttpContentLanguage(NSString* expected_http_content_language) {
NSString* const kMetaTagContentJS =
@"__gCrWeb.languageDetection.getMetaContentByHttpEquiv("
@"'content-language');";
InjectJsAndVerify(kMetaTagContentJS, expected_http_content_language);
}
// Verifies if |__gCrWeb.languageDetection.getTextContent| correctly extracts
// the text content from an HTML page.
void ExpectTextContent(NSString* expected_text_content) {
NSString* script = [[NSString alloc]
initWithFormat:
@"__gCrWeb.languageDetection.getTextContent(document.body, %lu);",
translate::kMaxIndexChars];
InjectJsAndVerify(script, expected_text_content);
}
};
// Tests |__gCrWeb.languageDetection.translationAllowed| JS call.
TEST_F(JsLanguageDetectionManagerTest, IsTranslationAllowed) {
LoadHtml(@"<html></html>");
ExpectTranslationAllowed(YES);
LoadHtml(@"<html><head>"
"<meta name='google' content='notranslate'>"
"</head></html>");
ExpectTranslationAllowed(NO);
LoadHtml(@"<html><head>"
"<meta name='google' value='notranslate'>"
"</head></html>");
ExpectTranslationAllowed(NO);
}
// Tests correctness of |document.documentElement.lang| attribute.
TEST_F(JsLanguageDetectionManagerTest, HtmlLang) {
NSString* html;
// Non-empty attribute.
html = [[NSString alloc]
initWithFormat:@"<html lang='%s'></html>", kExpectedLanguage];
LoadHtml(html);
ExpectHtmlLang(@(kExpectedLanguage));
// Empty attribute.
LoadHtml(@"<html></html>");
ExpectHtmlLang(@"");
// Test with mixed case.
html = [[NSString alloc]
initWithFormat:@"<html lAnG='%s'></html>", kExpectedLanguage];
LoadHtml(html);
ExpectHtmlLang(@(kExpectedLanguage));
}
// Tests |__gCrWeb.languageDetection.getMetaContentByHttpEquiv| JS call.
TEST_F(JsLanguageDetectionManagerTest, HttpContentLanguage) {
// No content language.
LoadHtml(@"<html></html>");
ExpectHttpContentLanguage(@"");
NSString* html;
// Some content language.
html = ([[NSString alloc]
initWithFormat:@"<html><head>"
"<meta http-equiv='content-language' content='%s'>"
"</head></html>",
kExpectedLanguage]);
LoadHtml(html);
ExpectHttpContentLanguage(@(kExpectedLanguage));
// Test with mixed case.
html = ([[NSString alloc]
initWithFormat:@"<html><head>"
"<meta http-equiv='cOnTenT-lAngUAge' content='%s'>"
"</head></html>",
kExpectedLanguage]);
LoadHtml(html);
ExpectHttpContentLanguage(@(kExpectedLanguage));
}
// Tests |__gCrWeb.languageDetection.getTextContent| JS call.
TEST_F(JsLanguageDetectionManagerTest, ExtractTextContent) {
LoadHtml(@"<html><body>"
"<script>var text = 'No scripts!'</script>"
"<p style='display: none;'>Not displayed!</p>"
"<p style='visibility: hidden;'>Hidden!</p>"
"<div>Some <span>text here <b>and</b></span> there.</div>"
"</body></html>");
ExpectTextContent(@"\nSome text here and there.");
}
// Tests that |__gCrWeb.languageDetection.getTextContent| correctly truncates
// text.
TEST_F(JsLanguageDetectionManagerTest, Truncation) {
LoadHtml(@"<html><body>"
"<script>var text = 'No scripts!'</script>"
"<p style='display: none;'>Not displayed!</p>"
"<p style='visibility: hidden;'>Hidden!</p>"
"<div>Some <span>text here <b>and</b></span> there.</div>"
"</body></html>");
NSString* const kTextContentJS =
@"__gCrWeb.languageDetection.getTextContent(document.body, 13)";
InjectJsAndVerify(kTextContentJS, @"\nSome text he");
}
// HTML elements introduce a line break, except inline ones.
TEST_F(JsLanguageDetectionManagerTest, ExtractWhitespace) {
// |b| and |span| do not break lines.
// |br| and |div| do.
LoadHtml(@"<html><body>"
"O<b>n</b>e<br>Two\tT<span>hr</span>ee<div>Four</div>"
"</body></html>");
ExpectTextContent(@"One\nTwo\tThree\nFour");
// |a| does not break lines.
// |li|, |p| and |ul| do.
LoadHtml(@"<html><body>"
"<ul><li>One</li><li>T<a href='foo'>wo</a></li></ul><p>Three</p>"
"</body></html>");
ExpectTextContent(@"\n\nOne\nTwo\nThree");
}
// Tests that |__gCrWeb.languageDetection.getTextContent| returns only until the
// kMaxIndexChars number of characters even if the text content is very large.
TEST_F(JsLanguageDetectionManagerTest, LongTextContent) {
// Very long string.
NSUInteger kLongStringLength = translate::kMaxIndexChars - 5;
NSMutableString* long_string = [GetLongString(kLongStringLength) mutableCopy];
[long_string appendString:@" b cdefghijklmnopqrstuvwxyz"];
// The string should be cut at the last whitespace, after the 'b' character.
NSString* html = [[NSString alloc]
initWithFormat:@"<html><body>%@</html></body>", long_string];
LoadHtml(html);
NSString* script = [[NSString alloc]
initWithFormat:
@"__gCrWeb.languageDetection.getTextContent(document.body, %lu);",
translate::kMaxIndexChars];
NSString* result = ExecuteJavaScript(script);
EXPECT_EQ(translate::kMaxIndexChars, [result length]);
}
// Tests if |__gCrWeb.languageDetection.retrieveBufferedTextContent| correctly
// retrieves the cache and then purges it.
TEST_F(JsLanguageDetectionManagerTest, RetrieveBufferedTextContent) {
LoadHtml(@"<html></html>");
// Set some cached text content.
ExecuteJavaScript(@"__gCrWeb.languageDetection.bufferedTextContent = 'foo'");
ExecuteJavaScript(@"__gCrWeb.languageDetection.activeRequests = 1");
NSString* const kRetrieveBufferedTextContentJS =
@"__gCrWeb.languageDetection.retrieveBufferedTextContent()";
InjectJsAndVerify(kRetrieveBufferedTextContentJS, @"foo");
// Verify cache is purged.
InjectJsAndVerify(@"__gCrWeb.languageDetection.bufferedTextContent",
[NSNull null]);
}
// Test fixture to test |__gCrWeb.languageDetection.detectLanguage|.
class JsLanguageDetectionManagerDetectLanguageTest
: public JsLanguageDetectionManagerTest {
public:
void SetUp() override {
JsLanguageDetectionManagerTest::SetUp();
auto callback = base::BindRepeating(
&JsLanguageDetectionManagerDetectLanguageTest::CommandReceived,
base::Unretained(this));
subscription_ =
web_state()->AddScriptCommandCallback(callback, "languageDetection");
}
// Called when "languageDetection" command is received.
void CommandReceived(const base::Value& command,
const GURL& url,
bool user_is_interacting,
web::WebFrame* sender_frame) {
commands_received_.push_back(command.Clone());
}
protected:
// Received "languageDetection" commands.
std::vector<base::Value> commands_received_;
// Subscription for JS message.
base::CallbackListSubscription subscription_;
};
// Tests if |__gCrWeb.languageDetection.detectLanguage| correctly informs the
// native side when translation is not allowed.
TEST_F(JsLanguageDetectionManagerDetectLanguageTest,
DetectLanguageTranslationNotAllowed) {
LoadHtml(@"<html></html>");
ExecuteJavaScript(@"__gCrWeb.languageDetection.detectLanguage()");
// Wait until the original injection has received a command.
base::test::ios::WaitUntilCondition(^bool() {
return !commands_received_.empty();
});
ASSERT_EQ(1U, commands_received_.size());
commands_received_.clear();
// Stub out translationAllowed.
NSString* const kTranslationAllowedJS =
@"__gCrWeb.languageDetection.translationAllowed = function() {"
@" return false;"
@"}";
ExecuteJavaScript(kTranslationAllowedJS);
ConditionBlock commands_recieved_block = ^bool {
return commands_received_.size();
};
InjectJSAndWaitUntilCondition(@"__gCrWeb.languageDetection.detectLanguage()",
commands_recieved_block);
ASSERT_EQ(1U, commands_received_.size());
const base::Value& value = commands_received_[0];
absl::optional<bool> translation_allowed =
value.FindBoolKey("translationAllowed");
ASSERT_TRUE(translation_allowed);
EXPECT_FALSE(*translation_allowed);
}
// Tests if |__gCrWeb.languageDetection.detectLanguage| correctly informs the
// native side when translation is allowed with the right parameters.
TEST_F(JsLanguageDetectionManagerDetectLanguageTest,
DetectLanguageTranslationAllowed) {
// A simple page that allows translation.
NSString* html = @"<html><head>"
@"<meta http-equiv='content-language' content='en'>"
@"</head></html>";
LoadHtml(html);
ExecuteJavaScript(@"__gCrWeb.languageDetection.detectLanguage()");
// Wait until the original injection has received a command.
base::test::ios::WaitUntilCondition(^bool() {
return !commands_received_.empty();
});
ASSERT_EQ(1U, commands_received_.size());
commands_received_.clear();
ExecuteJavaScript(@"__gCrWeb.languageDetection.detectLanguage()");
base::test::ios::WaitUntilCondition(^bool() {
return !commands_received_.empty();
});
ASSERT_EQ(1U, commands_received_.size());
const base::Value& value = commands_received_[0];
absl::optional<bool> translation_allowed =
value.FindBoolKey("translationAllowed");
ASSERT_TRUE(translation_allowed);
EXPECT_TRUE(value.FindKey("captureTextTime"));
EXPECT_TRUE(value.FindKey("htmlLang"));
EXPECT_TRUE(value.FindKey("httpContentLanguage"));
EXPECT_TRUE(*translation_allowed);
}