1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
|
// 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 "ash/constants/ash_switches.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 "components/user_manager/test_helper.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_web_ui.h"
#include "google_apis/gaia/gaia_id.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[] = "owner@gmail.com";
constexpr char kTestUserHash[] = "1234567890-hash";
constexpr GaiaId::Literal 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}, {});
// Use a persisted user.
command_line->AppendSwitchASCII(ash::switches::kLoginUser, kTestUserName);
command_line->AppendSwitchASCII(ash::switches::kLoginProfile,
kTestUserHash);
}
void SetUpLocalStatePrefService(PrefService* local_state) override {
InProcessBrowserTest::SetUpLocalStatePrefService(local_state);
// Register a persisted user.
user_manager::TestHelper::RegisterPersistedUser(
*local_state,
AccountId::FromUserEmailGaiaId(kTestUserName, kTestUserGaiaId));
}
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);
}
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_;
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) {
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
|