chromium/chrome/browser/spellchecker/spellcheck_service_browsertest.cc

// 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 <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/first_run/first_run.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/spellchecker/spell_check_host_chrome_impl.h"
#include "chrome/browser/spellchecker/spell_check_initialization_host_impl.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/common/spellcheck.mojom.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/common/spellcheck_result.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "components/sync/base/command_line_switches.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_utils.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/ash_features.h"
#endif

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#include "components/spellcheck/common/spellcheck_features.h"
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

BrowserContext;
RenderProcessHost;

class SpellcheckServiceBrowserTest : public InProcessBrowserTest,
                                     public spellcheck::mojom::SpellChecker {};

class SpellcheckServiceHostBrowserTest : public SpellcheckServiceBrowserTest {};

// Disable spell check should disable spelling service
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       DisableSpellcheckDisableSpellingService) {}

#if !BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       DisableSpellcheckIfDictionaryIsEmpty) {}
#endif  // !BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_CHROMEOS_ASH)
// Removing a spellcheck language from accept languages should not remove it
// from spellcheck languages list on CrOS.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       RemoveSpellcheckLanguageFromAcceptLanguages) {
  InitSpellcheck(true, "", "en-US,fr");
  SetAcceptLanguages("en-US,es,ru");
  EXPECT_EQ("en-US,fr", GetMultilingualDictionaries());
}
#else
// Removing a spellcheck language from accept languages should remove it from
// spellcheck languages list as well.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       RemoveSpellcheckLanguageFromAcceptLanguages) {}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

// Keeping spellcheck languages in accept languages should not alter spellcheck
// languages list.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       KeepSpellcheckLanguagesInAcceptLanguages) {}

// Starting with spellcheck enabled should send the 'enable spellcheck' message
// to the renderer. Consequently disabling spellcheck should send the 'disable
// spellcheck' message to the renderer.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, StartWithSpellcheck) {}

// Starting with only a single-language spellcheck setting should send the
// 'enable spellcheck' message to the renderer. Consequently removing spellcheck
// languages should disable spellcheck.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       StartWithSingularLanguagePreference) {}

// Starting with a multi-language spellcheck setting should send the 'enable
// spellcheck' message to the renderer. Consequently removing spellcheck
// languages should disable spellcheck.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       StartWithMultiLanguagePreference) {}

// Starting with both single-language and multi-language spellcheck settings
// should send the 'enable spellcheck' message to the renderer. Consequently
// removing spellcheck languages should disable spellcheck.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       StartWithBothLanguagePreferences) {}

// Starting without spellcheck languages should send the 'disable spellcheck'
// message to the renderer. Consequently adding spellchecking languages should
// enable spellcheck.
// Flaky, see https://crbug.com/600153
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       DISABLED_StartWithoutLanguages) {}

// Starting with spellcheck disabled should send the 'disable spellcheck'
// message to the renderer. Consequently enabling spellcheck should send the
// 'enable spellcheck' message to the renderer.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, StartWithoutSpellcheck) {}

// A custom dictionary state change should send a 'custom dictionary changed'
// message to the renderer, regardless of the spellcheck enabled state.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, CustomDictionaryChanged) {}

// Regression test for https://crbug.com/854540.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       CustomDictionaryChangedAfterRendererCrash) {}

// Starting with only a single-language spellcheck setting, the host should
// initialize the renderer's spellcheck system, and the same if the renderer
// explicity requests the spellcheck dictionaries.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceHostBrowserTest, RequestDictionary) {}

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
// When the renderer requests the spelling service for correcting text, the
// render process host should call the remote spelling service.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceHostBrowserTest, CallSpellingService) {}
#endif  // BUILDFLAG(USE_RENDERER_SPELLCHECKER)

// Tests that we can delete a corrupted BDICT file used by hunspell. We do not
// run this test on Mac because Mac does not use hunspell by default.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, DeleteCorruptedBDICT) {}

// Checks that preferences migrate correctly.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, PreferencesMigrated) {}

// Checks that preferences are not migrated when they shouldn't be.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest, PreferencesNotMigrated) {}

// Checks that, if a user has spellchecking disabled, nothing changes
// during migration.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       SpellcheckingDisabledPreferenceMigration) {}

// Make sure preferences get preserved and spellchecking stays enabled.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceBrowserTest,
                       MultilingualPreferenceNotMigrated) {}

#if BUILDFLAG(IS_WIN)
class SpellcheckServiceWindowsHybridBrowserTest
    : public SpellcheckServiceBrowserTest {
 public:
  SpellcheckServiceWindowsHybridBrowserTest()
      : SpellcheckServiceBrowserTest(/* use_browser_spell_checker=*/true) {}
};

IN_PROC_BROWSER_TEST_F(SpellcheckServiceWindowsHybridBrowserTest,
                       WindowsHybridSpellcheck) {
  // This test specifically covers the case where spellcheck delayed
  // initialization is not enabled, so return early if it is. Other tests
  // cover the case where delayed initialization is enabled.
  if (base::FeatureList::IsEnabled(spellcheck::kWinDelaySpellcheckServiceInit))
    return;

  ASSERT_TRUE(spellcheck::UseBrowserSpellChecker());

  // Note that the base class forces dictionary sync to not be performed, which
  // on its own would have created a SpellcheckService object. So testing here
  // that we are still instantiating the SpellcheckService as a browser startup
  // task to support hybrid spellchecking.
  SpellcheckService* service = static_cast<SpellcheckService*>(
      SpellcheckServiceFactory::GetInstance()->GetServiceForBrowserContext(
          GetContext(), /* create */ false));
  ASSERT_NE(nullptr, service);

  // The list of Windows spellcheck languages should have been populated by at
  // least one language. This assures that the spellcheck context menu will
  // include Windows spellcheck languages that lack Hunspell support.
  EXPECT_TRUE(service->dictionaries_loaded());
  EXPECT_FALSE(service->windows_spellcheck_dictionary_map_.empty());
}

class SpellcheckServiceWindowsHybridBrowserTestDelayInit
    : public SpellcheckServiceBrowserTest {
 public:
  SpellcheckServiceWindowsHybridBrowserTestDelayInit()
      : SpellcheckServiceBrowserTest(/* use_browser_spell_checker=*/true) {}

  void SetUp() override {
    // Don't initialize the SpellcheckService on browser launch.
    feature_list_.InitAndEnableFeature(
        spellcheck::kWinDelaySpellcheckServiceInit);

    // Add command line switch that forces first run state, to test whether
    // primary preferred language has its spellcheck dictionary enabled by
    // default for non-Hunspell languages.
    first_run::ResetCachedSentinelDataForTesting();
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kForceFirstRun);

    InProcessBrowserTest::SetUp();
  }

  void OnDictionariesInitialized() {
    dictionaries_initialized_received_ = true;
    if (quit_on_callback_)
      std::move(quit_on_callback_).Run();
  }

 protected:
  void RunUntilCallbackReceived() {
    if (dictionaries_initialized_received_)
      return;
    base::RunLoop run_loop;
    quit_on_callback_ = 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_on_callback_;
};

// Used for faking the presence of Windows spellcheck dictionaries.
const std::vector<std::string> kWindowsSpellcheckLanguages = {
    "fi-FI",  // Finnish has no Hunspell support.
    "fr-FR",  // French has both Windows and Hunspell support.
    "pt-BR"   // Portuguese (Brazil) has both Windows and Hunspell support, but
              // generic pt does not have Hunspell support.
};

// Used for testing whether primary preferred language is enabled by default for
// spellchecking.
const char kAcceptLanguages[] = "fi-FI,fi,ar-AR,fr-FR,fr,hr,ceb,pt-BR,pt";
const std::vector<std::string> kSpellcheckDictionariesBefore = {
    // Note that Finnish is initially unset, but has Windows spellcheck
    // dictionary present.
    "ar",     // Arabic has no Hunspell support, and its Windows spellcheck
              // dictionary is not present.
    "fr-FR",  // French has both Windows and Hunspell support, and its Windows
              // spellcheck dictionary is present.
    "fr",     // Generic language should also be toggleable for spellcheck.
    "hr",     // Croatian has Hunspell support.
    "ceb",    // Cebuano doesn't have any dictionary support and should be
              // removed from preferences.
    "pt-BR",  // Portuguese (Brazil) has both Windows and Hunspell support, and
              // its Windows spellcheck dictionary is present.
    "pt"      // Generic language should also be toggleable for spellcheck.
};

const std::vector<std::string> kSpellcheckDictionariesAfter = {
    "fi",     // Finnish should have been enabled for spellchecking since
              // it's the primary language.
    "fr-FR",  // French should still be there.
    "fr",     // Should still be entry for generic French.
    "hr",     // So should Croatian.
    "pt-BR",  // Portuguese (Brazil) should still be there.
    "pt"      // Should still be entry for generic Portuguese.
};

// As a prelude to the next test, sets the initial accept languages and
// spellcheck language preferences for the test profile.
IN_PROC_BROWSER_TEST_F(SpellcheckServiceWindowsHybridBrowserTestDelayInit,
                       PRE_WindowsHybridSpellcheckDelayInit) {
  GetPrefs()->SetString(language::prefs::kSelectedLanguages, kAcceptLanguages);
  base::Value::List spellcheck_dictionaries_list;
  for (const auto& dictionary : kSpellcheckDictionariesBefore) {
    spellcheck_dictionaries_list.Append(std::move(dictionary));
  }
  GetPrefs()->SetList(spellcheck::prefs::kSpellCheckDictionaries,
                      std::move(spellcheck_dictionaries_list));
}

IN_PROC_BROWSER_TEST_F(SpellcheckServiceWindowsHybridBrowserTestDelayInit,
                       WindowsHybridSpellcheckDelayInit) {
  ASSERT_TRUE(spellcheck::UseBrowserSpellChecker());

  // Note that the base class forces dictionary sync to not be performed, and
  // the kWinDelaySpellcheckServiceInit flag is set, which together should
  // prevent creation of a SpellcheckService object on browser startup. So
  // testing here that this is indeed the case.
  SpellcheckService* service = static_cast<SpellcheckService*>(
      SpellcheckServiceFactory::GetInstance()->GetServiceForBrowserContext(
          GetContext(), /* create */ false));
  EXPECT_EQ(nullptr, service);

  // Now create the SpellcheckService but don't call InitializeDictionaries().
  service = static_cast<SpellcheckService*>(
      SpellcheckServiceFactory::GetInstance()->GetServiceForBrowserContext(
          GetContext(), /* create */ true));

  ASSERT_NE(nullptr, service);

  // The list of Windows spellcheck languages should not have been populated
  // yet since InitializeDictionaries() has not been called.
  EXPECT_FALSE(service->dictionaries_loaded());
  EXPECT_TRUE(service->windows_spellcheck_dictionary_map_.empty());

  // Fake the presence of Windows spellcheck dictionaries.
  service->AddSpellcheckLanguagesForTesting(kWindowsSpellcheckLanguages);

  service->InitializeDictionaries(
      base::BindOnce(&SpellcheckServiceWindowsHybridBrowserTestDelayInit::
                         OnDictionariesInitialized,
                     base::Unretained(this)));

  RunUntilCallbackReceived();
  EXPECT_TRUE(service->dictionaries_loaded());
  // The list of Windows spellcheck languages should now have been populated.
  std::map<std::string, std::string>
      windows_spellcheck_dictionary_map_first_call =
          service->windows_spellcheck_dictionary_map_;
  EXPECT_FALSE(windows_spellcheck_dictionary_map_first_call.empty());

  // Check that the primary accept language has spellchecking enabled and
  // that languages with no spellcheck support have spellchecking disabled.
  EXPECT_EQ(kAcceptLanguages,
            GetPrefs()->GetString(language::prefs::kAcceptLanguages));
  const base::Value::List& dictionaries_list =
      GetPrefs()->GetList(spellcheck::prefs::kSpellCheckDictionaries);
  std::vector<std::string> actual_dictionaries;
  for (const auto& dictionary : dictionaries_list) {
    actual_dictionaries.push_back(dictionary.GetString());
  }
  EXPECT_EQ(kSpellcheckDictionariesAfter, actual_dictionaries);

  // It should be safe to call InitializeDictionaries again (it should
  // immediately run the callback).
  service->InitializeDictionaries(
      base::BindOnce(&SpellcheckServiceWindowsHybridBrowserTestDelayInit::
                         OnDictionariesInitialized,
                     base::Unretained(this)));

  RunUntilCallbackReceived();
  EXPECT_TRUE(service->dictionaries_loaded());
  EXPECT_EQ(windows_spellcheck_dictionary_map_first_call,
            service->windows_spellcheck_dictionary_map_);
}
#endif  // BUILDFLAG(IS_WIN)