blob: bb6eff28cf84f5ac2e2692e09d95cc28a7b30208 [file] [log] [blame]
// Copyright 2018 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>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/api/language_settings_private/language_settings_private_api.h"
#include "chrome/browser/extensions/api/language_settings_private/language_settings_private_delegate.h"
#include "chrome/browser/extensions/api/language_settings_private/language_settings_private_delegate_factory.h"
#include "chrome/browser/extensions/extension_function_test_utils.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/test_browser_window.h"
#include "chrome/test/base/testing_profile.h"
#include "components/crx_file/id_util.h"
#include "components/language/core/browser/pref_names.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/extension_prefs.h"
#if defined(OS_CHROMEOS)
#include "ui/base/ime/chromeos/component_extension_ime_manager.h"
#include "ui/base/ime/chromeos/extension_ime_util.h"
#include "ui/base/ime/chromeos/fake_input_method_delegate.h"
#include "ui/base/ime/chromeos/input_method_util.h"
#include "ui/base/ime/chromeos/mock_component_extension_ime_manager_delegate.h"
#include "ui/base/ime/chromeos/mock_input_method_manager.h"
#include "ui/base/l10n/l10n_util.h"
#endif
namespace extensions {
typedef api::language_settings_private::SpellcheckDictionaryStatus
DictionaryStatus;
class MockLanguageSettingsPrivateDelegate
: public LanguageSettingsPrivateDelegate {
public:
explicit MockLanguageSettingsPrivateDelegate(content::BrowserContext* context)
: LanguageSettingsPrivateDelegate(context) {}
~MockLanguageSettingsPrivateDelegate() override {}
// LanguageSettingsPrivateDelegate:
std::vector<DictionaryStatus> GetHunspellDictionaryStatuses() override;
void RetryDownloadHunspellDictionary(const std::string& language) override;
std::vector<std::string> retry_download_hunspell_dictionary_called_with() {
return retry_download_hunspell_dictionary_called_with_;
}
private:
std::vector<std::string> retry_download_hunspell_dictionary_called_with_;
};
std::vector<DictionaryStatus>
MockLanguageSettingsPrivateDelegate::GetHunspellDictionaryStatuses() {
std::vector<DictionaryStatus> statuses;
DictionaryStatus status;
status.language_code = "fr";
status.is_ready = false;
status.is_downloading = std::make_unique<bool>(true);
status.download_failed = std::make_unique<bool>(false);
statuses.push_back(std::move(status));
return statuses;
}
void MockLanguageSettingsPrivateDelegate::RetryDownloadHunspellDictionary(
const std::string& language) {
retry_download_hunspell_dictionary_called_with_.push_back(language);
}
namespace {
std::unique_ptr<KeyedService> BuildEventRouter(
content::BrowserContext* profile) {
return std::make_unique<EventRouter>(profile, ExtensionPrefs::Get(profile));
}
std::unique_ptr<KeyedService> BuildLanguageSettingsPrivateDelegate(
content::BrowserContext* profile) {
return std::make_unique<MockLanguageSettingsPrivateDelegate>(profile);
}
std::unique_ptr<KeyedService> BuildSpellcheckService(
content::BrowserContext* profile) {
return std::make_unique<SpellcheckService>(static_cast<Profile*>(profile));
}
} // namespace
class LanguageSettingsPrivateApiTest : public ExtensionServiceTestBase {
public:
LanguageSettingsPrivateApiTest() = default;
~LanguageSettingsPrivateApiTest() override = default;
protected:
void RunGetLanguageListTest();
virtual void InitFeatures() {
#if defined(OS_WIN)
// Force Windows hybrid spellcheck to be enabled.
feature_list_.InitAndEnableFeature(spellcheck::kWinUseBrowserSpellChecker);
#endif // defined(OS_WIN)
}
#if defined(OS_WIN)
virtual void AddSpellcheckLanguagesForTesting(
const std::vector<std::string>& spellcheck_languages_for_testing) {
SpellcheckServiceFactory::GetInstance()
->GetForContext(profile())
->InitWindowsDictionaryLanguages(spellcheck_languages_for_testing);
}
base::test::ScopedFeatureList feature_list_;
#endif // defined(OS_WIN)
private:
void SetUp() override {
ExtensionServiceTestBase::SetUp();
ExtensionServiceTestBase::InitializeEmptyExtensionService();
EventRouterFactory::GetInstance()->SetTestingFactory(
profile(), base::BindRepeating(&BuildEventRouter));
InitFeatures();
LanguageSettingsPrivateDelegateFactory::GetInstance()->SetTestingFactory(
profile(), base::BindRepeating(&BuildLanguageSettingsPrivateDelegate));
// Use SetTestingFactoryAndUse to force creation and initialization.
SpellcheckServiceFactory::GetInstance()->SetTestingFactoryAndUse(
profile(), base::BindRepeating(&BuildSpellcheckService));
}
std::unique_ptr<TestBrowserWindow> browser_window_;
std::unique_ptr<Browser> browser_;
};
TEST_F(LanguageSettingsPrivateApiTest, RetryDownloadHunspellDictionaryTest) {
MockLanguageSettingsPrivateDelegate* mock_delegate =
static_cast<MockLanguageSettingsPrivateDelegate*>(
LanguageSettingsPrivateDelegateFactory::GetForBrowserContext(
browser_context()));
auto function = base::MakeRefCounted<
LanguageSettingsPrivateRetryDownloadDictionaryFunction>();
EXPECT_EQ(
0u,
mock_delegate->retry_download_hunspell_dictionary_called_with().size());
EXPECT_TRUE(
api_test_utils::RunFunction(function.get(), "[\"fr\"]", profile()))
<< function->GetError();
EXPECT_EQ(
1u,
mock_delegate->retry_download_hunspell_dictionary_called_with().size());
EXPECT_EQ(
"fr",
mock_delegate->retry_download_hunspell_dictionary_called_with().front());
}
TEST_F(LanguageSettingsPrivateApiTest, GetSpellcheckDictionaryStatusesTest) {
auto function = base::MakeRefCounted<
LanguageSettingsPrivateGetSpellcheckDictionaryStatusesFunction>();
std::unique_ptr<base::Value> actual =
api_test_utils::RunFunctionAndReturnSingleResult(function.get(), "[]",
profile());
EXPECT_TRUE(actual) << function->GetError();
base::ListValue expected;
auto expected_status = std::make_unique<base::DictionaryValue>();
expected_status->SetString("languageCode", "fr");
expected_status->SetBoolean("isReady", false);
expected_status->SetBoolean("isDownloading", true);
expected_status->SetBoolean("downloadFailed", false);
expected.Append(std::move(expected_status));
EXPECT_EQ(expected, *actual);
}
TEST_F(LanguageSettingsPrivateApiTest, GetLanguageListTest) {
RunGetLanguageListTest();
}
void LanguageSettingsPrivateApiTest::RunGetLanguageListTest() {
struct LanguageToTest {
std::string accept_language;
std::string windows_dictionary_name; // Empty string indicates to not use
// fake Windows dictionary
bool is_preferred_language;
bool is_spellcheck_support_expected;
};
std::vector<LanguageToTest> languages_to_test = {
// Languages with both Windows and Hunspell spellcheck support.
// GetLanguageList should always report spellchecking to be supported for
// these languages, regardless of whether a language pack is installed or
// if it is a preferred language.
{"fr", "fr-FR", true, true},
{"de", "de-DE", false, true},
{"es-MX", "", true, true},
{"fa", "", false, true},
{"gl", "", true, false},
{"zu", "", false, false},
// Finnish with Filipino language pack (string in string).
{"fi", "fil", true, false},
// Sesotho with Asturian language pack (string in string).
{"st", "ast", true, false},
};
// A few more test cases for non-Hunspell languages. These languages do have
// Windows spellcheck support depending on the OS version. GetLanguageList
// only reports spellchecking is supported for these languages if the language
// pack is installed.
#if defined(OS_WIN)
if (spellcheck::WindowsVersionSupportsSpellchecker()) {
languages_to_test.push_back({"ar", "ar-SA", true, true});
languages_to_test.push_back({"bn", "bn-IN", false, true});
} else {
languages_to_test.push_back({"ar", "ar-SA", true, false});
languages_to_test.push_back({"bn", "bn-IN", false, false});
}
#else
languages_to_test.push_back({"ar", "ar-SA", true, false});
languages_to_test.push_back({"bn", "bn-IN", false, false});
#endif // defined(OS_WIN)
// Initialize accept languages prefs.
std::vector<std::string> accept_languages;
for (auto& language_to_test : languages_to_test) {
if (language_to_test.is_preferred_language) {
accept_languages.push_back(language_to_test.accept_language);
}
}
std::string accept_languages_string = base::JoinString(accept_languages, ",");
DVLOG(2) << "Setting accept languages preferences to: "
<< accept_languages_string;
profile()->GetPrefs()->SetString(language::prefs::kAcceptLanguages,
accept_languages_string);
#if defined(OS_WIN)
// Add fake Windows dictionaries using InitWindowsDictionaryLanguages.
std::vector<std::string> windows_spellcheck_languages_for_testing;
for (auto& language_to_test : languages_to_test) {
if (!language_to_test.windows_dictionary_name.empty()) {
windows_spellcheck_languages_for_testing.push_back(
language_to_test.windows_dictionary_name);
DVLOG(2) << "Will set fake Windows spellcheck dictionary for testing: "
<< language_to_test.windows_dictionary_name;
}
}
AddSpellcheckLanguagesForTesting(windows_spellcheck_languages_for_testing);
#endif // defined(OS_WIN)
auto function =
base::MakeRefCounted<LanguageSettingsPrivateGetLanguageListFunction>();
std::unique_ptr<base::Value> result =
api_test_utils::RunFunctionAndReturnSingleResult(function.get(), "[]",
profile());
ASSERT_NE(nullptr, result) << function->GetError();
EXPECT_TRUE(result->is_list());
size_t languages_to_test_found_count = 0;
for (auto& language_val : result->GetList()) {
EXPECT_TRUE(language_val.is_dict());
std::string* language_code_ptr = language_val.FindStringKey("code");
ASSERT_NE(nullptr, language_code_ptr);
std::string language_code = *language_code_ptr;
EXPECT_FALSE(language_code.empty());
const base::Optional<bool> maybe_supports_spellcheck =
language_val.FindBoolKey("supportsSpellcheck");
const bool supports_spellcheck = maybe_supports_spellcheck.has_value()
? maybe_supports_spellcheck.value()
: false;
for (auto& language_to_test : languages_to_test) {
if (language_to_test.accept_language == language_code) {
DVLOG(2) << "*** Found language code being tested=" << language_code
<< ", supportsSpellcheck=" << supports_spellcheck << " ***";
EXPECT_EQ(language_to_test.is_spellcheck_support_expected,
supports_spellcheck);
languages_to_test_found_count++;
break;
}
}
}
EXPECT_EQ(languages_to_test.size(), languages_to_test_found_count);
}
#if defined(OS_CHROMEOS)
namespace {
namespace input_method = chromeos::input_method;
using input_method::InputMethodDescriptor;
using input_method::InputMethodManager;
std::string GetExtensionImeId() {
std::string kExtensionImeId = chromeos::extension_ime_util::GetInputMethodID(
crx_file::id_util::GenerateId("test.extension.ime"), "us");
return kExtensionImeId;
}
std::string GetComponentExtensionImeId() {
std::string kComponentExtensionImeId =
chromeos::extension_ime_util::GetComponentInputMethodID(
crx_file::id_util::GenerateId("test.component.extension.ime"), "us");
return kComponentExtensionImeId;
}
std::string GetArcImeId() {
std::string kArcImeId = chromeos::extension_ime_util::GetArcInputMethodID(
crx_file::id_util::GenerateId("test.arc.ime"), "us");
return kArcImeId;
}
class TestInputMethodManager : public input_method::MockInputMethodManager {
public:
class TestState : public input_method::MockInputMethodManager::State {
public:
TestState() {
// Set up three IMEs
std::vector<std::string> layouts({"us"});
std::vector<std::string> languages({"en-US", "en"});
std::vector<std::string> arc_languages(
{chromeos::extension_ime_util::kArcImeLanguage});
InputMethodDescriptor extension_ime(
GetExtensionImeId(), "ExtensionIme", "", layouts, languages,
false /* is_login_keyboard */, GURL(), GURL());
InputMethodDescriptor component_extension_ime(
GetComponentExtensionImeId(), "ComponentExtensionIme", "", layouts,
languages, false /* is_login_keyboard */, GURL(), GURL());
InputMethodDescriptor arc_ime(
GetArcImeId(), "ArcIme", "", layouts, arc_languages,
false /* is_login_keyboard */, GURL(), GURL());
input_methods_ = {extension_ime, component_extension_ime, arc_ime};
}
void GetInputMethodExtensions(
input_method::InputMethodDescriptors* descriptors) override {
for (const auto& descriptor : input_methods_)
descriptors->push_back(descriptor);
}
input_method::InputMethodDescriptors input_methods_;
protected:
friend base::RefCounted<InputMethodManager::State>;
~TestState() override = default;
DISALLOW_COPY_AND_ASSIGN(TestState);
};
TestInputMethodManager() : state_(new TestState), util_(&delegate_) {
util_.AppendInputMethods(state_->input_methods_);
mock_delegate_ =
new chromeos::input_method::MockComponentExtIMEManagerDelegate();
component_ext_mgr_ =
std::make_unique<chromeos::ComponentExtensionIMEManager>();
component_ext_mgr_->Initialize(base::WrapUnique(mock_delegate_));
}
scoped_refptr<InputMethodManager::State> GetActiveIMEState() override {
return state_;
}
input_method::InputMethodUtil* GetInputMethodUtil() override {
return &util_;
}
chromeos::ComponentExtensionIMEManager* GetComponentExtensionIMEManager()
override {
return component_ext_mgr_.get();
}
private:
scoped_refptr<TestState> state_;
input_method::FakeInputMethodDelegate delegate_;
input_method::InputMethodUtil util_;
std::unique_ptr<chromeos::ComponentExtensionIMEManager> component_ext_mgr_;
chromeos::input_method::MockComponentExtIMEManagerDelegate* mock_delegate_;
DISALLOW_COPY_AND_ASSIGN(TestInputMethodManager);
};
} // namespace
TEST_F(LanguageSettingsPrivateApiTest, GetInputMethodListsTest) {
TestInputMethodManager::Initialize(new TestInputMethodManager);
// Initialize relevant prefs.
StringPrefMember enabled_imes;
enabled_imes.Init(prefs::kLanguageEnabledImes, profile()->GetPrefs());
StringPrefMember preload_engines;
preload_engines.Init(prefs::kLanguagePreloadEngines, profile()->GetPrefs());
enabled_imes.SetValue(
base::JoinString({GetExtensionImeId(), GetArcImeId()}, ","));
preload_engines.SetValue(GetComponentExtensionImeId());
auto function = base::MakeRefCounted<
LanguageSettingsPrivateGetInputMethodListsFunction>();
std::unique_ptr<base::Value> result =
api_test_utils::RunFunctionAndReturnSingleResult(function.get(), "[]",
profile());
ASSERT_NE(nullptr, result) << function->GetError();
EXPECT_TRUE(result->is_dict());
base::Value* input_methods = result->FindListKey("thirdPartyExtensionImes");
ASSERT_NE(input_methods, nullptr);
EXPECT_EQ(3u, input_methods->GetList().size());
for (auto& input_method : input_methods->GetList()) {
base::Value* ime_tags_ptr = input_method.FindListKey("tags");
ASSERT_NE(nullptr, ime_tags_ptr);
// Check tags contain input method's display name
base::Value* ime_name_ptr = input_method.FindKey("displayName");
EXPECT_TRUE(base::Contains(ime_tags_ptr->GetList(), *ime_name_ptr));
// Check tags contain input method's language codes' display names
base::Value* ime_language_codes_ptr =
input_method.FindListKey("languageCodes");
ASSERT_NE(nullptr, ime_language_codes_ptr);
for (auto& language_code : ime_language_codes_ptr->GetList()) {
std::string language_display_name =
base::UTF16ToUTF8(l10n_util::GetDisplayNameForLocale(
language_code.GetString(), "en", true));
if (!language_display_name.empty())
EXPECT_TRUE(base::Contains(ime_tags_ptr->GetList(),
base::Value(language_display_name)));
}
}
TestInputMethodManager::Shutdown();
}
TEST_F(LanguageSettingsPrivateApiTest, AddInputMethodTest) {
TestInputMethodManager::Initialize(new TestInputMethodManager);
// Initialize relevant prefs.
profile()->GetPrefs()->SetString(language::prefs::kPreferredLanguages,
"en-US");
StringPrefMember enabled_imes;
enabled_imes.Init(prefs::kLanguageEnabledImes, profile()->GetPrefs());
StringPrefMember preload_engines;
preload_engines.Init(prefs::kLanguagePreloadEngines, profile()->GetPrefs());
enabled_imes.SetValue(std::string());
preload_engines.SetValue(std::string());
{
// Add an extension IME. kLanguageEnabledImes should be updated.
auto function =
base::MakeRefCounted<LanguageSettingsPrivateAddInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetExtensionImeId() + "\"]", profile());
EXPECT_EQ(GetExtensionImeId(), enabled_imes.GetValue());
EXPECT_TRUE(preload_engines.GetValue().empty());
}
enabled_imes.SetValue(std::string());
preload_engines.SetValue(std::string());
{
// Add a component extension IME. kLanguagePreloadEngines should be
// updated.
auto function =
base::MakeRefCounted<LanguageSettingsPrivateAddInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetComponentExtensionImeId() + "\"]",
profile());
EXPECT_TRUE(enabled_imes.GetValue().empty());
EXPECT_EQ(GetComponentExtensionImeId(), preload_engines.GetValue());
}
enabled_imes.SetValue(std::string());
preload_engines.SetValue(std::string());
{
// Add an ARC IME. kLanguageEnabledImes should be updated.
auto function =
base::MakeRefCounted<LanguageSettingsPrivateAddInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetArcImeId() + "\"]", profile());
EXPECT_EQ(GetArcImeId(), enabled_imes.GetValue());
EXPECT_TRUE(preload_engines.GetValue().empty());
}
TestInputMethodManager::Shutdown();
}
TEST_F(LanguageSettingsPrivateApiTest, RemoveInputMethodTest) {
TestInputMethodManager::Initialize(new TestInputMethodManager);
// Initialize relevant prefs.
StringPrefMember enabled_imes;
enabled_imes.Init(prefs::kLanguageEnabledImes, profile()->GetPrefs());
StringPrefMember preload_engines;
preload_engines.Init(prefs::kLanguagePreloadEngines, profile()->GetPrefs());
enabled_imes.SetValue(
base::JoinString({GetExtensionImeId(), GetArcImeId()}, ","));
preload_engines.SetValue(GetComponentExtensionImeId());
{
// Remove an extension IME.
auto function = base::MakeRefCounted<
LanguageSettingsPrivateRemoveInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetExtensionImeId() + "\"]", profile());
EXPECT_EQ(GetArcImeId(), enabled_imes.GetValue());
EXPECT_EQ(GetComponentExtensionImeId(), preload_engines.GetValue());
}
{
// Remove a component extension IME.
auto function = base::MakeRefCounted<
LanguageSettingsPrivateRemoveInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetComponentExtensionImeId() + "\"]",
profile());
EXPECT_EQ(GetArcImeId(), enabled_imes.GetValue());
EXPECT_TRUE(preload_engines.GetValue().empty());
}
{
// Remove an ARC IME.
auto function = base::MakeRefCounted<
LanguageSettingsPrivateRemoveInputMethodFunction>();
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), "[\"" + GetArcImeId() + "\"]", profile());
EXPECT_TRUE(enabled_imes.GetValue().empty());
EXPECT_TRUE(preload_engines.GetValue().empty());
}
TestInputMethodManager::Shutdown();
}
#endif // OS_CHROMEOS
#if defined(OS_WIN)
class LanguageSettingsPrivateApiTestDelayInit
: public LanguageSettingsPrivateApiTest {
public:
LanguageSettingsPrivateApiTestDelayInit() = default;
protected:
void InitFeatures() override {
// Force Windows hybrid spellcheck and delayed initialization of the
// spellcheck service to be enabled.
feature_list_.InitWithFeatures(
/*enabled_features=*/{spellcheck::kWinUseBrowserSpellChecker,
spellcheck::kWinDelaySpellcheckServiceInit},
/*disabled_features=*/{});
}
void AddSpellcheckLanguagesForTesting(
const std::vector<std::string>& spellcheck_languages_for_testing)
override {
SpellcheckServiceFactory::GetInstance()
->GetForContext(profile())
->AddSpellcheckLanguagesForTesting(spellcheck_languages_for_testing);
}
};
TEST_F(LanguageSettingsPrivateApiTestDelayInit, GetLanguageListTest) {
RunGetLanguageListTest();
}
#endif // defined(OS_WIN)
} // namespace extensions