blob: 68c479ca1e8fd93ef661258823122cbc6277ad95 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include <optional>
#include <ostream>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/supports_user_data.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
struct TestCase {
TestCase(
const std::string& accept_languages,
const std::vector<std::string>& spellcheck_dictionaries,
const std::vector<std::string>& expected_languages,
const std::vector<std::string>& expected_languages_used_for_spellcheck)
: accept_languages(accept_languages),
spellcheck_dictionaries(spellcheck_dictionaries) {
SpellcheckService::Dictionary dictionary;
for (const auto& language : expected_languages) {
if (!language.empty()) {
dictionary.language = language;
dictionary.used_for_spellcheck =
base::Contains(expected_languages_used_for_spellcheck, language);
expected_dictionaries.push_back(dictionary);
}
}
}
~TestCase() = default;
std::string accept_languages;
std::vector<std::string> spellcheck_dictionaries;
std::vector<SpellcheckService::Dictionary> expected_dictionaries;
};
bool operator==(const SpellcheckService::Dictionary& lhs,
const SpellcheckService::Dictionary& rhs) {
return lhs.language == rhs.language &&
lhs.used_for_spellcheck == rhs.used_for_spellcheck;
}
std::ostream& operator<<(std::ostream& out,
const SpellcheckService::Dictionary& dictionary) {
out << "{\"" << dictionary.language << "\", used_for_spellcheck="
<< (dictionary.used_for_spellcheck ? "true " : "false") << "}";
return out;
}
std::ostream& operator<<(std::ostream& out, const TestCase& test_case) {
out << "language::prefs::kAcceptLanguages=[" << test_case.accept_languages
<< "], prefs::kSpellCheckDictionaries=["
<< base::JoinString(test_case.spellcheck_dictionaries, ",")
<< "], expected=[";
for (const auto& dictionary : test_case.expected_dictionaries) {
out << dictionary << ",";
}
out << "]";
return out;
}
static std::unique_ptr<KeyedService> BuildSpellcheckService(
content::BrowserContext* profile) {
return std::make_unique<SpellcheckService>(static_cast<Profile*>(profile));
}
class SpellcheckServiceUnitTestBase : public testing::Test {
public:
SpellcheckServiceUnitTestBase() = default;
SpellcheckServiceUnitTestBase(const SpellcheckServiceUnitTestBase&) = delete;
SpellcheckServiceUnitTestBase& operator=(
const SpellcheckServiceUnitTestBase&) = delete;
~SpellcheckServiceUnitTestBase() override = default;
content::BrowserContext* browser_context() { return &profile_; }
PrefService* prefs() { return profile_.GetPrefs(); }
protected:
void SetUp() override {
// Use SetTestingFactoryAndUse to force creation and initialization.
SpellcheckServiceFactory::GetInstance()->SetTestingFactoryAndUse(
&profile_, base::BindRepeating(&BuildSpellcheckService));
}
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
};
class SpellcheckServiceUnitTest : public SpellcheckServiceUnitTestBase,
public testing::WithParamInterface<TestCase> {
private:
#if BUILDFLAG(IS_WIN)
// Tests were designed assuming Hunspell dictionary used and may fail when
// Windows spellcheck is enabled by default.
spellcheck::ScopedDisableBrowserSpellCheckerForTesting
disable_browser_spell_checker_;
#endif // BUILDFLAG(IS_WIN)
};
INSTANTIATE_TEST_SUITE_P(
TestCases,
SpellcheckServiceUnitTest,
testing::Values(
TestCase("en-JP,aa", {"aa"}, {}, {}),
TestCase("en,aa", {"aa"}, {"en"}, {}),
TestCase("en,en-JP,fr,aa", {"fr"}, {"en", "fr"}, {"fr"}),
TestCase("en,en-JP,fr,zz,en-US", {"fr"}, {"en", "fr", "en-US"}, {"fr"}),
TestCase("en,en-US,en-GB",
{"en-GB"},
{"en", "en-US", "en-GB"},
{"en-GB"}),
TestCase("en,en-US,en-AU",
{"en-AU"},
{"en", "en-US", "en-AU"},
{"en-AU"}),
TestCase("en,en-US,en-AU",
{"en-US"},
{"en", "en-US", "en-AU"},
{"en-US"}),
TestCase("en,en-US", {"en-US"}, {"en", "en-US"}, {"en-US"}),
TestCase("en,en-US,fr", {"en-US"}, {"en", "en-US", "fr"}, {"en-US"}),
TestCase("en,fr,en-US,en-AU",
{"en-US", "fr"},
{"en", "fr", "en-US", "en-AU"},
{"fr", "en-US"}),
TestCase("en-US,en", {"en-US"}, {"en-US", "en"}, {"en-US"}),
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
// Scenario where user disabled the Windows spellcheck feature with some
// non-Hunspell languages set in preferences.
TestCase("fr,eu,en-US,ar",
{"fr", "eu", "en-US", "ar"},
{"fr", "en-US"},
{"fr", "en-US"}),
#endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
TestCase("hu-HU,hr-HR", {"hr"}, {"hu", "hr"}, {"hr"})));
TEST_P(SpellcheckServiceUnitTest, GetDictionaries) {
prefs()->SetString(language::prefs::kAcceptLanguages,
GetParam().accept_languages);
base::Value::List spellcheck_dictionaries;
for (const std::string& dictionary : GetParam().spellcheck_dictionaries) {
spellcheck_dictionaries.Append(dictionary);
}
prefs()->SetList(spellcheck::prefs::kSpellCheckDictionaries,
std::move(spellcheck_dictionaries));
std::vector<SpellcheckService::Dictionary> dictionaries;
SpellcheckService::GetDictionaries(browser_context(), &dictionaries);
EXPECT_EQ(GetParam().expected_dictionaries, dictionaries);
}
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
class SpellcheckServiceHybridUnitTestBase
: public SpellcheckServiceUnitTestBase {
public:
SpellcheckServiceHybridUnitTestBase() = default;
protected:
void SetUp() override {
InitFeatures();
// Use SetTestingFactoryAndUse to force creation and initialization.
SpellcheckServiceFactory::GetInstance()->SetTestingFactoryAndUse(
&profile_, base::BindRepeating(&BuildSpellcheckService));
}
virtual void InitFeatures() {}
virtual void InitializeSpellcheckService(
const std::vector<std::string>& spellcheck_languages_for_testing) {
// Fake the presence of Windows spellcheck dictionaries.
spellcheck_service_ =
SpellcheckServiceFactory::GetInstance()->GetForContext(
browser_context());
spellcheck_service_->InitWindowsDictionaryLanguages(
spellcheck_languages_for_testing);
ASSERT_TRUE(spellcheck_service_->dictionaries_loaded());
}
void RunGetDictionariesTest(
const std::string accept_languages,
const std::vector<std::string> spellcheck_dictionaries,
const std::vector<SpellcheckService::Dictionary> expected_dictionaries);
void RunDictionaryMappingTest(
const std::string full_tag,
const std::string expected_accept_language,
const std::string expected_tag_passed_to_spellcheck,
const std::string expected_accept_language_generic,
const std::string expected_tag_passed_to_spellcheck_generic);
// Used for faking the presence of Windows spellcheck dictionaries.
static const std::vector<std::string>
windows_spellcheck_languages_for_testing_;
base::test::ScopedFeatureList feature_list_;
raw_ptr<SpellcheckService> spellcheck_service_;
};
void SpellcheckServiceHybridUnitTestBase::RunGetDictionariesTest(
const std::string accept_languages,
const std::vector<std::string> spellcheck_dictionaries,
const std::vector<SpellcheckService::Dictionary> expected_dictionaries) {
prefs()->SetString(language::prefs::kAcceptLanguages, accept_languages);
base::Value::List spellcheck_dictionaries_list;
for (std::string dict : spellcheck_dictionaries) {
spellcheck_dictionaries_list.Append(dict);
}
prefs()->SetList(spellcheck::prefs::kSpellCheckDictionaries,
std::move(spellcheck_dictionaries_list));
// Simulate first-run scenario (method is normally called during browser
// start-up). If the primary accept language has no dictionary support, it is
// expected that spellchecking will be disabled for that language.
SpellcheckService::EnableFirstUserLanguageForSpellcheck(prefs());
InitializeSpellcheckService(windows_spellcheck_languages_for_testing_);
std::vector<SpellcheckService::Dictionary> dictionaries;
SpellcheckService::GetDictionaries(browser_context(), &dictionaries);
EXPECT_EQ(expected_dictionaries, dictionaries);
}
void SpellcheckServiceHybridUnitTestBase::RunDictionaryMappingTest(
const std::string full_tag,
const std::string expected_accept_language,
const std::string expected_tag_passed_to_spellcheck,
const std::string expected_accept_language_generic,
const std::string expected_tag_passed_to_spellcheck_generic) {
InitializeSpellcheckService({full_tag});
std::string supported_dictionary;
if (!expected_accept_language.empty()) {
supported_dictionary =
spellcheck_service_->GetSupportedWindowsDictionaryLanguage(
expected_accept_language);
EXPECT_FALSE(supported_dictionary.empty());
EXPECT_EQ(full_tag, supported_dictionary);
EXPECT_EQ(expected_tag_passed_to_spellcheck,
SpellcheckService::GetTagToPassToWindowsSpellchecker(
expected_accept_language, full_tag));
// Special case for Serbian. The "sr" accept language is interpreted as
// using Cyrillic script. There should be an extra entry in the windows
// dictionary map if Cyrillic windows dictionary is installed.
if (base::EqualsCaseInsensitiveASCII(
"sr-Cyrl", SpellcheckService::GetLanguageAndScriptTag(
full_tag,
/* include_script_tag= */ true))) {
EXPECT_EQ(
full_tag,
spellcheck_service_->GetSupportedWindowsDictionaryLanguage("sr"));
} else {
EXPECT_TRUE(
spellcheck_service_->GetSupportedWindowsDictionaryLanguage("sr")
.empty());
}
if (!expected_accept_language_generic.empty()) {
supported_dictionary =
spellcheck_service_->GetSupportedWindowsDictionaryLanguage(
expected_accept_language_generic);
EXPECT_FALSE(supported_dictionary.empty());
EXPECT_EQ(expected_accept_language_generic, supported_dictionary);
EXPECT_EQ(expected_tag_passed_to_spellcheck_generic,
SpellcheckService::GetTagToPassToWindowsSpellchecker(
expected_accept_language_generic, supported_dictionary));
} else {
// Should only be one entry in the map.
EXPECT_EQ(1u,
spellcheck_service_->windows_spellcheck_dictionary_map_.size());
}
} else {
// Unsupported language--should not be in map.
EXPECT_TRUE(
spellcheck_service_->windows_spellcheck_dictionary_map_.empty());
}
}
// static
const std::vector<std::string> SpellcheckServiceHybridUnitTestBase::
windows_spellcheck_languages_for_testing_ = {
"fr-FR", // Has both Windows and Hunspell support.
"es-MX", // Has both Windows and Hunspell support, but for Hunspell
// maps to es-ES.
"gl-ES", // (Galician) Has only Windows support, no Hunspell
// dictionary.
"fi-FI", // (Finnish) Has only Windows support, no Hunspell
// dictionary.
"it-IT", // Has both Windows and Hunspell support.
"pt-BR", // Has both Windows and Hunspell support.
"haw-US", // (Hawaiian) No Hunspell dictionary. Note that first two
// letters of language code are "ha," the same as Hausa.
"ast", // (Asturian) Has only Windows support, no Hunspell
// dictionary. Note that last two letters of language
// code are "st," the same as Sesotho.
"kok-Deva-IN", // Konkani (Devanagari, India)--note presence of
// script subtag.
"sr-Cyrl-ME", // Serbian (Cyrillic, Montenegro)--note presence of
// script subtag.
"sr-Latn-ME", // Serbian (Latin, Montenegro)--note presence of
// script subtag.
"ja-Latn-JP-x-ext", // Japanese with Latin script--note presence of
// private use subtag. Ignore private use
// dictionaries.
};
class GetDictionariesHybridUnitTestNoDelayInit
: public SpellcheckServiceHybridUnitTestBase,
public testing::WithParamInterface<TestCase> {
protected:
void InitFeatures() override {
// Disable kWinDelaySpellcheckServiceInit, as the case where it's enabled
// is tested in SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit.
feature_list_.InitAndDisableFeature(
spellcheck::kWinDelaySpellcheckServiceInit);
}
};
static const TestCase kHybridGetDictionariesParams[] = {
// Galician (gl) has only Windows support, no Hunspell dictionary. Croatian
// (hr) has only Hunspell support, no local Windows dictionary. First
// language is supported by windows and should be spellchecked.
TestCase("gl", {}, {"gl"}, {"gl"}),
TestCase("gl", {"gl"}, {"gl"}, {"gl"}),
TestCase("gl,hr", {}, {"gl", "hr"}, {"gl"}),
TestCase("gl,hr", {"gl"}, {"gl", "hr"}, {"gl"}),
TestCase("gl,hr", {"hr"}, {"gl", "hr"}, {"gl", "hr"}),
TestCase("gl,hr", {"gl", "hr"}, {"gl", "hr"}, {"gl", "hr"}),
TestCase("hr", {}, {"hr"}, {"hr"}),
TestCase("hr", {"hr"}, {"hr"}, {"hr"}),
TestCase("hr,gl", {"hr"}, {"hr", "gl"}, {"hr"}),
// Cebuano (ceb) is a language with neither Windows or Hunspell support,
// should be unset if was enabled during simulated "first run" scenario.
TestCase("ceb", {}, {}, {}),
TestCase("ceb,gl,hr", {"gl", "hr"}, {"gl", "hr"}, {"gl", "hr"}),
// Finnish has only "fi" in hard-coded list of accept languages.
TestCase("fi-FI,fi,en-US,en",
{"en-US"},
{"fi", "en-US", "en"},
{"fi", "en-US"}),
// First language is supported by Windows but private use dictionaries
// are ignored.
TestCase("ja,gl", {"gl"}, {"gl"}, {"gl"}),
// (Basque) No Hunspell support, has Windows support but
// language pack not present.
TestCase("eu", {"eu"}, {}, {}),
TestCase("es-419,es-MX",
{"es-419", "es-MX"},
{"es-419", "es-MX"},
{"es-419", "es-MX"}),
TestCase("fr-FR,es-MX,gl,pt-BR,hr,it",
{"fr-FR", "gl", "pt-BR", "it"},
{"fr-FR", "es-MX", "gl", "pt-BR", "hr", "it"},
{"fr-FR", "gl", "pt-BR", "it"}),
// Hausa with Hawaiian language pack (ha/haw string in string).
TestCase("ha", {"ha"}, {}, {}),
// Sesotho with Asturian language pack (st/ast string in string).
TestCase("st", {"st"}, {}, {}),
// User chose generic Serbian in languages preferences (which uses
// Cyrillic script).
TestCase("sr,sr-Latn-RS", {"sr", "sr-Latn-RS"}, {"sr"}, {"sr"}),
// If there is platform spellcheck support for a regional variation of
// a language, the generic version should also be toggleable in spellcheck
// settings. There is no Hunspell dictionary for generic Portuguese (pt);
// there is Hunspell support for generic Italian (it) but the platform
// dictionary should be used instead.
TestCase("pt,pt-BR", {"pt", "pt-BR"}, {"pt", "pt-BR"}, {"pt", "pt-BR"}),
TestCase("it,it-IT", {"it", "it-IT"}, {"it", "it-IT"}, {"it", "it-IT"}),
};
INSTANTIATE_TEST_SUITE_P(TestCases,
GetDictionariesHybridUnitTestNoDelayInit,
testing::ValuesIn(kHybridGetDictionariesParams));
TEST_P(GetDictionariesHybridUnitTestNoDelayInit, GetDictionaries) {
RunGetDictionariesTest(GetParam().accept_languages,
GetParam().spellcheck_dictionaries,
GetParam().expected_dictionaries);
}
struct DictionaryMappingTestCase {
std::string full_tag;
std::string expected_accept_language;
std::string expected_tag_passed_to_spellcheck;
std::string expected_accept_language_generic;
std::string expected_tag_passed_to_spellcheck_generic;
};
std::ostream& operator<<(std::ostream& out,
const DictionaryMappingTestCase& test_case) {
out << "full_tag=" << test_case.full_tag
<< ", expected_accept_language=" << test_case.expected_accept_language
<< ", expected_tag_passed_to_spellcheck="
<< test_case.expected_tag_passed_to_spellcheck
<< ", expected_accept_language_generic="
<< test_case.expected_accept_language_generic
<< ", expected_tag_passed_to_spellcheck_generic="
<< test_case.expected_tag_passed_to_spellcheck_generic;
return out;
}
class SpellcheckServiceWindowsDictionaryMappingUnitTest
: public SpellcheckServiceHybridUnitTestBase,
public testing::WithParamInterface<DictionaryMappingTestCase> {
protected:
void InitFeatures() override {
// Disable kWinDelaySpellcheckServiceInit, as the case where it's enabled
// is tested in SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit.
feature_list_.InitAndDisableFeature(
spellcheck::kWinDelaySpellcheckServiceInit);
}
};
static const DictionaryMappingTestCase kHybridDictionaryMappingsParams[] = {
DictionaryMappingTestCase({"en-CA", "en-CA", "en-CA", "en", "en"}),
DictionaryMappingTestCase({"en-PH", "en", "en", "", ""}),
DictionaryMappingTestCase({"es-MX", "es-MX", "es-MX", "es", "es"}),
DictionaryMappingTestCase({"ar-SA", "ar", "ar", "", ""}),
DictionaryMappingTestCase({"kok-Deva-IN", "kok", "kok-Deva", "", ""}),
DictionaryMappingTestCase({"sr-Cyrl-RS", "sr", "sr-Cyrl", "", ""}),
DictionaryMappingTestCase({"sr-Cyrl-ME", "sr", "sr-Cyrl", "", ""}),
// Only sr with Cyrillic implied supported in Chromium.
DictionaryMappingTestCase({"sr-Latn-RS", "", "sr-Latn", "", ""}),
DictionaryMappingTestCase({"sr-Latn-ME", "", "sr-Latn", "", ""}),
DictionaryMappingTestCase({"ca-ES", "ca", "ca", "", ""}),
DictionaryMappingTestCase({"ca-ES-valencia", "ca", "ca", "", ""}),
// If there is platform spellcheck support for a regional variation of
// a language, the generic version should also be toggleable in spellcheck
// settings. There is no Hunspell dictionary for generic Portuguese (pt);
// there is Hunspell support for generic Italian (it) but the platform
// dictionary should be used instead.
DictionaryMappingTestCase({"it-IT", "it-IT", "it-IT", "it", "it"}),
DictionaryMappingTestCase({"pt-BR", "pt-BR", "pt-BR", "pt", "pt"}),
};
INSTANTIATE_TEST_SUITE_P(TestCases,
SpellcheckServiceWindowsDictionaryMappingUnitTest,
testing::ValuesIn(kHybridDictionaryMappingsParams));
TEST_P(SpellcheckServiceWindowsDictionaryMappingUnitTest, CheckMappings) {
RunDictionaryMappingTest(
GetParam().full_tag, GetParam().expected_accept_language,
GetParam().expected_tag_passed_to_spellcheck,
GetParam().expected_accept_language_generic,
GetParam().expected_tag_passed_to_spellcheck_generic);
}
class SpellcheckServiceHybridUnitTestDelayInitBase
: public SpellcheckServiceHybridUnitTestBase {
public:
SpellcheckServiceHybridUnitTestDelayInitBase() = default;
void OnDictionariesInitialized() {
dictionaries_initialized_received_ = true;
if (quit_)
std::move(quit_).Run();
}
protected:
void InitFeatures() override {
// Don't initialize the SpellcheckService on browser launch.
feature_list_.InitAndEnableFeature(
spellcheck::kWinDelaySpellcheckServiceInit);
}
void InitializeSpellcheckService(
const std::vector<std::string>& spellcheck_languages_for_testing)
override {
// Fake the presence of Windows spellcheck dictionaries.
spellcheck_service_ =
SpellcheckServiceFactory::GetInstance()->GetForContext(
browser_context());
spellcheck_service_->AddSpellcheckLanguagesForTesting(
spellcheck_languages_for_testing);
// Asynchronously load the dictionaries.
ASSERT_FALSE(spellcheck_service_->dictionaries_loaded());
spellcheck_service_->InitializeDictionaries(
base::BindOnce(&SpellcheckServiceHybridUnitTestDelayInitBase::
OnDictionariesInitialized,
base::Unretained(this)));
RunUntilCallbackReceived();
ASSERT_TRUE(spellcheck_service_->dictionaries_loaded());
}
void RunUntilCallbackReceived() {
if (dictionaries_initialized_received_)
return;
base::RunLoop run_loop;
quit_ = run_loop.QuitClosure();
run_loop.Run();
// reset status.
dictionaries_initialized_received_ = false;
}
private:
bool dictionaries_initialized_received_ = false;
// Quits the RunLoop on receiving the callback from InitializeDictionaries.
base::OnceClosure quit_;
};
class SpellcheckServiceHybridUnitTestDelayInit
: public SpellcheckServiceHybridUnitTestDelayInitBase,
public testing::WithParamInterface<TestCase> {};
INSTANTIATE_TEST_SUITE_P(TestCases,
SpellcheckServiceHybridUnitTestDelayInit,
testing::ValuesIn(kHybridGetDictionariesParams));
TEST_P(SpellcheckServiceHybridUnitTestDelayInit, GetDictionaries) {
RunGetDictionariesTest(GetParam().accept_languages,
GetParam().spellcheck_dictionaries,
GetParam().expected_dictionaries);
}
class SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit
: public SpellcheckServiceHybridUnitTestDelayInitBase,
public testing::WithParamInterface<DictionaryMappingTestCase> {};
INSTANTIATE_TEST_SUITE_P(
TestCases,
SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit,
testing::ValuesIn(kHybridDictionaryMappingsParams));
TEST_P(SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit,
CheckMappings) {
RunDictionaryMappingTest(
GetParam().full_tag, GetParam().expected_accept_language,
GetParam().expected_tag_passed_to_spellcheck,
GetParam().expected_accept_language_generic,
GetParam().expected_tag_passed_to_spellcheck_generic);
}
#endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)