chromium/chrome/browser/ui/webui/ash/settings/pages/a11y/accessibility_handler_browsertest.cc

// Copyright 2021 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/ui/webui/ash/settings/pages/a11y/accessibility_handler.h"

#include <memory>
#include <optional>
#include <set>
#include <string_view>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/containers/adapters.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/input_method/mock_input_method_engine.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/locale_util.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/ime/ash/input_method_descriptor.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/ime/ash/input_method_util.h"

using ::testing::Contains;
using ::testing::Not;

namespace ash::settings {

namespace {

// Use a real domain to avoid policy loading problems.
constexpr char kTestUserName[] = "[email protected]";
constexpr char kTestUserGaiaId[] = "9876543210";

}  // namespace

class TestAccessibilityHandler : public AccessibilityHandler {
 public:
  explicit TestAccessibilityHandler(Profile* profile)
      : AccessibilityHandler(profile) {}
  ~TestAccessibilityHandler() override = default;
};

class AccessibilityHandlerTest : public InProcessBrowserTest {
 public:
  AccessibilityHandlerTest()
      : mock_ime_engine_handler_(
            std::make_unique<input_method::MockInputMethodEngine>()) {}
  AccessibilityHandlerTest(const AccessibilityHandlerTest&) = delete;
  AccessibilityHandlerTest& operator=(const AccessibilityHandlerTest&) = delete;
  ~AccessibilityHandlerTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    scoped_feature_list_.InitWithFeatures(
        {features::kOnDeviceSpeechRecognition}, {});
  }

  void SetUpOnMainThread() override {
    handler_ = std::make_unique<TestAccessibilityHandler>(browser()->profile());
    handler_->set_web_ui(&web_ui_);
    handler_->RegisterMessages();
    handler_->AllowJavascriptForTesting();
    base::RunLoop().RunUntilIdle();

    // Set the Dictation locale for tests.
    SetDictationLocale("en-US");
  }

  void TearDownOnMainThread() override {
    handler_->DisallowJavascript();
    handler_.reset();
  }

  size_t GetNumWebUICalls() { return web_ui_.call_data().size(); }

  void AssertWebUICalls(unsigned int num) {
    ASSERT_EQ(num, web_ui_.call_data().size());
  }

  bool WasWebUIListenerCalledWithStringArgument(
      const std::string& expected_listener,
      const std::string& expected_argument) {
    for (const std::unique_ptr<content::TestWebUI::CallData>& data :
         base::Reversed(web_ui_.call_data())) {
      std::string listener = data->arg1()->GetString();
      if (!data->arg2()->is_string()) {
        // Only look for listeners with a single string argument. Continue
        // silently if we find anything else.
        continue;
      }
      std::string listener_argument = data->arg2()->GetString();

      if (data->function_name() == "cr.webUIListenerCallback" &&
          listener == expected_listener &&
          expected_argument == listener_argument) {
        return true;
      }
    }

    return false;
  }

  bool GetWebUIListenerArgumentListValue(const std::string& expected_listener,
                                         const base::Value::List*& argument) {
    for (const std::unique_ptr<content::TestWebUI::CallData>& data :
         base::Reversed(web_ui_.call_data())) {
      std::string listener;
      if (data->arg1()->is_string()) {
        listener = data->arg1()->GetString();
      }
      if (data->function_name() == "cr.webUIListenerCallback" &&
          listener == expected_listener) {
        if (!data->arg2()->is_list()) {
          return false;
        }
        argument = &data->arg2()->GetList();
        return true;
      }
    }

    return false;
  }

  void MaybeAddDictationLocales() { handler_->MaybeAddDictationLocales(); }

  void SetDictationLocale(const std::string& locale) {
    ProfileManager::GetActiveUserProfile()->GetPrefs()->SetString(
        prefs::kAccessibilityDictationLocale, locale);
  }

  void CreateSession(const AccountId& account_id) {
    auto* session_manager = session_manager::SessionManager::Get();
    session_manager->CreateSession(account_id, account_id.GetUserEmail(),
                                   false);
  }

  void StartUserSession(const AccountId& account_id) {
    profiles::testing::CreateProfileSync(
        g_browser_process->profile_manager(),
        BrowserContextHelper::Get()->GetBrowserContextPathByUserIdHash(
            user_manager::UserManager::Get()
                ->FindUser(account_id)
                ->username_hash()));

    auto* session_manager = session_manager::SessionManager::Get();
    session_manager->NotifyUserProfileLoaded(account_id);
    session_manager->SessionStarted();
  }

  speech::SodaInstaller* soda_installer() {
    return speech::SodaInstaller::GetInstance();
  }

  speech::LanguageCode en_us() { return speech::LanguageCode::kEnUs; }
  speech::LanguageCode fr_fr() { return speech::LanguageCode::kFrFr; }
  content::TestWebUI* web_ui() { return &web_ui_; }

  std::unique_ptr<input_method::MockInputMethodEngine> mock_ime_engine_handler_;

  const AccountId test_account_id_ =
      AccountId::FromUserEmailGaiaId(kTestUserName, kTestUserGaiaId);

 private:
  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<TestAccessibilityHandler> handler_;
  content::TestWebUI web_ui_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Ensures that AccessibilityHandler listens to SODA download state changes, and
// fires the correct listener when SODA AND the language pack matching the
// Dictation locale are installed.
IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, OnSodaInstalledNotification) {
  SetDictationLocale("fr-FR");
  size_t num_calls = GetNumWebUICalls();
  // Pretend that the SODA binary was installed. We still need to wait for the
  // correct language pack before doing anything.
  soda_installer()->NotifySodaInstalledForTesting();
  AssertWebUICalls(num_calls);
  soda_installer()->NotifySodaInstalledForTesting(en_us());
  AssertWebUICalls(num_calls);
  soda_installer()->NotifySodaInstalledForTesting(fr_fr());
  AssertWebUICalls(num_calls + 1);
  ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
      "dictation-locale-menu-subtitle-changed",
      "French (France) is processed locally and works offline"));
}

// Verifies that the correct string is sent to the JavaScript end of the web UI.
// Ensures we only notify the user of progress for the language pack matching
// the Dictation locale.
IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, OnSodaProgressNotification) {
  size_t num_calls = GetNumWebUICalls();
  soda_installer()->NotifySodaProgressForTesting(50, fr_fr());
  AssertWebUICalls(num_calls);
  soda_installer()->NotifySodaProgressForTesting(50, en_us());
  AssertWebUICalls(num_calls + 1);
  soda_installer()->NotifySodaProgressForTesting(50);
  AssertWebUICalls(num_calls + 2);
  ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
      "dictation-locale-menu-subtitle-changed",
      "Downloading speech recognition files… 50%"));
}

// Verifies that the correct string is sent to the JavaScript end of the web UI
// when the SODA binary fails to download.
IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, OnSodaErrorNotification) {
  size_t num_calls = GetNumWebUICalls();
  soda_installer()->NotifySodaErrorForTesting();
  AssertWebUICalls(num_calls + 1);
  ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
      "dictation-locale-menu-subtitle-changed",
      "Couldn’t download English (United States) speech files. Download will "
      "be attempted later. Speech is sent to Google for processing until "
      "download is completed."));
}

// Verifies that the correct listener is fired when the language pack matching
// the Dictation locale fails to download.
IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest,
                       OnSodaLanguageErrorNotification) {
  size_t num_calls = GetNumWebUICalls();
  // Do nothing if the failed language pack is different than the Dictation
  // locale.
  soda_installer()->NotifySodaErrorForTesting(fr_fr());
  AssertWebUICalls(num_calls);
  // Fire the correct listener when the language pack matching the Dictation
  // locale fails.
  soda_installer()->NotifySodaErrorForTesting(en_us());
  AssertWebUICalls(num_calls + 1);
  ASSERT_TRUE(WasWebUIListenerCalledWithStringArgument(
      "dictation-locale-menu-subtitle-changed",
      "Couldn’t download English (United States) speech files. Download will "
      "be attempted later. Speech is sent to Google for processing until "
      "download is completed."));
}

IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, DictationLocalesCalculation) {
  input_method::InputMethodManager* ime_manager =
      input_method::InputMethodManager::Get();

  struct {
    std::string application_locale;
    std::vector<std::string> ime_locales;
    std::string preferred_languages;
    std::set<std::string_view> expected_recommended_prefixes;
  } kTestCases[] = {
      {"en-US", {}, "", {"en"}},
      {"en", {}, "", {"en"}},
      {"fr", {}, "", {"fr"}},
      {"en", {"en", "en-US"}, "", {"en"}},
      {"en", {"en", "en-US"}, "en", {"en"}},
      {"en", {"en", "es"}, "", {"en", "es"}},
      {"en", {"fr", "es", "fr-FR"}, "", {"en", "es", "fr"}},
      {"it-IT", {"ar", "is-IS", "uk"}, "", {"it", "ar", "is", "uk"}},
      {"en", {"fr", "es", "fr-FR"}, "en-US,it-IT", {"en", "es", "fr", "it"}},
      {"en", {}, "en-US,it-IT,uk", {"en", "it", "uk"}},
  };
  for (const auto& testcase : kTestCases) {
    // Set application locale.
    g_browser_process->SetApplicationLocale(testcase.application_locale);

    // Set up fake IMEs.
    auto state =
        ime_manager->CreateNewState(ProfileManager::GetActiveUserProfile());
    ime_manager->SetState(state);
    input_method::InputMethodDescriptors imes;
    for (auto& locale : testcase.ime_locales) {
      std::string id = "fake-ime-extension-" + locale;
      input_method::InputMethodDescriptor descriptor(
          id, locale, std::string(), std::string(), {locale}, false, GURL(),
          GURL(), /*handwriting_language=*/std::nullopt);
      imes.push_back(descriptor);
    }
    ime_manager->GetInputMethodUtil()->ResetInputMethods(imes);

    for (auto& descriptor : imes) {
      state->AddInputMethodExtension(descriptor.id(), {descriptor},
                                     mock_ime_engine_handler_.get());
      ASSERT_TRUE(state->EnableInputMethod(descriptor.id()));
    }

    // Set up fake preferred languages.
    browser()->profile()->GetPrefs()->SetString(
        language::prefs::kPreferredLanguages, testcase.preferred_languages);

    MaybeAddDictationLocales();

    const base::Value::List* argument = nullptr;
    ASSERT_TRUE(
        GetWebUIListenerArgumentListValue("dictation-locales-set", argument));
    for (const base::Value& it : *argument) {
      const base::Value::Dict& dict = it.GetDict();
      std::string_view language_code =
          language::SplitIntoMainAndTail(*(dict.FindString("value"))).first;
      // Only expect some locales to be recommended based on application and
      // IME languages.
      if (*(dict.FindBool("recommended"))) {
        EXPECT_THAT(testcase.expected_recommended_prefixes,
                    Contains(language_code));
      } else {
        EXPECT_THAT(testcase.expected_recommended_prefixes,
                    Not(Contains(language_code)));
      }
    }
  }
}

IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest,
                       DictationLocalesOfflineAndInstalled) {
  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting();
  speech::SodaInstaller::GetInstance()->NotifySodaInstalledForTesting(en_us());
  MaybeAddDictationLocales();
  const base::Value::List* argument = nullptr;
  ASSERT_TRUE(
      GetWebUIListenerArgumentListValue("dictation-locales-set", argument));

  for (auto& it : *argument) {
    const base::Value::Dict& dict = it.GetDict();
    const std::string locale = *dict.FindString("value");
    bool works_offline = dict.FindBool("worksOffline").value();
    bool installed = dict.FindBool("installed").value();
    if (locale == speech::kUsEnglishLocale) {
      EXPECT_TRUE(works_offline);
      EXPECT_TRUE(installed);
    } else {
      // Some locales other than en-us can be installed offline, but should not
      // be.
      EXPECT_FALSE(installed) << " for locale " << locale;
    }
  }
}

IN_PROC_BROWSER_TEST_F(AccessibilityHandlerTest, GetStartupSoundEnabled) {
  CreateSession(test_account_id_);
  StartUserSession(test_account_id_);
  AccessibilityManager::Get()->SetStartupSoundEnabled(true);

  size_t call_data_count_before_call = web_ui()->call_data().size();

  base::Value::List empty_args;
  web_ui()->HandleReceivedMessage("getStartupSoundEnabled", empty_args);

  ASSERT_EQ(call_data_count_before_call + 1u, web_ui()->call_data().size());

  const content::TestWebUI::CallData& call_data =
      *(web_ui()->call_data()[call_data_count_before_call]);
  EXPECT_EQ("cr.webUIListenerCallback", call_data.function_name());
  EXPECT_EQ("startup-sound-setting-retrieved", call_data.arg1()->GetString());
  EXPECT_TRUE(call_data.arg2()->GetBool());
}

}  // namespace ash::settings