// 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 "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/ash/input_method/assistive_window_controller.h"
#include "chrome/browser/ash/input_method/native_input_method_engine.h"
#include "chrome/browser/ash/input_method/stub_input_method_engine_observer.h"
#include "chrome/browser/ash/input_method/suggestion_enums.h"
#include "chrome/browser/ash/input_method/textinput_test_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/browser/autofill_test_utils.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/input_method_ash.h"
#include "ui/base/ime/ash/mock_ime_input_context_handler.h"
#include "ui/base/ime/ash/text_input_method.h"
#include "ui/base/ime/dummy_text_input_client.h"
#include "ui/base/ime/ime_key_event_dispatcher.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
namespace ash {
namespace input_method {
namespace {
constexpr char kEmojiData[] = "happy,😀;😃;😄";
class TestObserver : public StubInputMethodEngineObserver {
public:
TestObserver() = default;
~TestObserver() override = default;
TestObserver(const TestObserver&) = delete;
TestObserver& operator=(const TestObserver&) = delete;
void OnKeyEvent(const std::string& engine_id,
const ui::KeyEvent& event,
TextInputMethod::KeyEventDoneCallback callback) override {
std::move(callback).Run(ui::ime::KeyEventHandledState::kNotHandled);
}
void OnInputMethodOptionsChanged(const std::string& engine_id) override {
changed_engine_id_ = engine_id;
}
void ClearChangedEngineId() { changed_engine_id_ = ""; }
const std::string& changed_engine_id() const { return changed_engine_id_; }
private:
std::string changed_engine_id_;
};
class KeyProcessingWaiter {
public:
TextInputMethod::KeyEventDoneCallback CreateCallback() {
return base::BindOnce(&KeyProcessingWaiter::OnKeyEventDone,
base::Unretained(this));
}
void OnKeyEventDone(ui::ime::KeyEventHandledState handled_state) {
run_loop_.Quit();
}
void Wait() { run_loop_.Run(); }
private:
base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
};
// These use the browser test framework but tamper with the environment through
// global singletons, effectively bypassing CrOS IMF "input method management".
// Test subject is a bespoke NativeInputMethodEngine instance manually attached
// to the environment, shadowing those created and managed by CrOS IMF (an
// integral part of the "browser" environment set up by the browser test).
// TODO(crbug/1197005): Migrate all these to unit tests.
class NativeInputMethodEngineWithoutImeServiceTest
: public InProcessBrowserTest,
public ui::ImeKeyEventDispatcher {
public:
NativeInputMethodEngineWithoutImeServiceTest() : input_method_(this) {
feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kOnDeviceGrammarCheck},
/*disabled_features=*/{});
}
protected:
void SetUp() override { InProcessBrowserTest::SetUp(); }
void SetUpOnMainThread() override {
// Passing |false| for |use_ime_service| so NativeInputMethodEngine won't
// launch the IME Service which typically tries to load libimedecoder.so
// unsupported in browser tests. None of these tests require the IME Service
// so just avoid it outright instead of relying on implicit luck.
engine_ =
NativeInputMethodEngine::CreateForTesting(/*use_ime_service=*/false);
IMEBridge::Get()->SetInputContextHandler(&input_method_);
IMEBridge::Get()->SetCurrentEngineHandler(engine_.get());
auto observer = std::make_unique<TestObserver>();
observer_ = observer.get();
profile_ = browser()->profile();
prefs_ = profile_->GetPrefs();
prefs_->Set(::prefs::kLanguageInputMethodSpecificSettings,
base::Value(base::Value::Type::DICT));
engine_->Initialize(std::move(observer), /*extension_id=*/"", profile_);
engine_->get_assistive_suggester_for_testing()
->get_emoji_suggester_for_testing()
->LoadEmojiMapForTesting(kEmojiData);
// Ensure predictive writing is off to stop tests from attempting to
// load the shared library.
prefs_->SetBoolean(prefs::kAssistPredictiveWritingEnabled, false);
InProcessBrowserTest::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
// Reset the engine before shutting down the browser because the engine
// observes ChromeKeyboardControllerClient, which is tied to the browser
// lifetime.
engine_.reset();
IMEBridge::Get()->SetInputContextHandler(nullptr);
IMEBridge::Get()->SetCurrentEngineHandler(nullptr);
InProcessBrowserTest::TearDownOnMainThread();
}
void SetUpTextInput(TextInputTestHelper& helper) {
GURL url = ui_test_utils::GetTestUrl(
base::FilePath(FILE_PATH_LITERAL("textinput")),
base::FilePath(FILE_PATH_LITERAL("simple_textarea.html")));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(
content::ExecJs(tab, "document.getElementById('text_id').focus()"));
helper.WaitForTextInputStateChanged(ui::TEXT_INPUT_TYPE_TEXT_AREA);
SetFocus(helper.GetTextInputClient());
}
// Overridden from ui::ImeKeyEventDispatcher:
ui::EventDispatchDetails DispatchKeyEventPostIME(
ui::KeyEvent* event) override {
return ui::EventDispatchDetails();
}
void DispatchKeyPress(ui::KeyboardCode code,
bool need_flush,
int flags = ui::EF_NONE) {
KeyProcessingWaiter waiterPressed;
KeyProcessingWaiter waiterReleased;
engine_->ProcessKeyEvent({ui::EventType::kKeyPressed, code, flags},
waiterPressed.CreateCallback());
engine_->ProcessKeyEvent({ui::EventType::kKeyReleased, code, flags},
waiterReleased.CreateCallback());
if (need_flush) {
engine_->FlushForTesting();
}
waiterPressed.Wait();
waiterReleased.Wait();
}
void DispatchKeyPresses(const std::vector<ui::KeyboardCode>& codes,
bool need_flush) {
for (const ui::KeyboardCode& code : codes) {
DispatchKeyPress(code, need_flush);
}
}
void SetFocus(ui::TextInputClient* client) {
input_method_.SetFocusedTextInputClient(client);
}
ui::InputMethod* GetBrowserInputMethod() {
return browser()->window()->GetNativeWindow()->GetHost()->GetInputMethod();
}
std::unique_ptr<NativeInputMethodEngine> engine_;
raw_ptr<Profile, DanglingUntriaged> profile_;
raw_ptr<PrefService, DanglingUntriaged> prefs_;
raw_ptr<TestObserver, DanglingUntriaged> observer_;
private:
InputMethodAsh input_method_;
base::test::ScopedFeatureList feature_list_;
};
// ID is specified in google_xkb_manifest.json.
constexpr char kEngineIdUs[] = "xkb:us::eng";
} // namespace
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
SuggestEmoji) {
base::HistogramTester histogram_tester;
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string prefix_text = u"happy ";
const std::u16string expected_result_text = u"happy 😀";
helper.GetTextInputClient()->InsertText(
prefix_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(prefix_text);
// Selects first emoji.
DispatchKeyPress(ui::VKEY_DOWN, false);
DispatchKeyPress(ui::VKEY_RETURN, false);
helper.WaitForSurroundingTextChanged(expected_result_text);
EXPECT_EQ(expected_result_text, helper.GetSurroundingText());
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Match",
AssistiveType::kEmoji, 1);
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Disabled.Emoji",
DisabledReason::kNone, 1);
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Coverage",
AssistiveType::kEmoji, 1);
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Success",
AssistiveType::kEmoji, 1);
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
DismissEmojiSuggestionWhenUsersContinueTyping) {
base::HistogramTester histogram_tester;
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string prefix_text = u"happy ";
const std::u16string expected_result_text = u"happy a";
helper.GetTextInputClient()->InsertText(
prefix_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(prefix_text);
// Types something random to dismiss emoji
helper.GetTextInputClient()->InsertText(
u"a",
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(expected_result_text);
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
EmojiSuggestionDisabledReasonkEnterpriseSettingsOff) {
base::HistogramTester histogram_tester;
prefs_->SetBoolean(prefs::kEmojiSuggestionEnterpriseAllowed, false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GURL(chrome::kChromeUINewTabURL)));
ui_test_utils::SendToOmniboxAndSubmit(browser(), "happy ");
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Disabled.Emoji",
DisabledReason::kEnterpriseSettingsOff,
1);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
EmojiSuggestionDisabledReasonkUserSettingsOff) {
base::HistogramTester histogram_tester;
prefs_->SetBoolean(prefs::kEmojiSuggestionEnabled, false);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GURL(chrome::kChromeUINewTabURL)));
ui_test_utils::SendToOmniboxAndSubmit(browser(), "happy ");
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Disabled.Emoji",
DisabledReason::kUserSettingsOff, 1);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
EmojiSuggestionDisabledReasonkUrlOrAppNotAllowed) {
base::HistogramTester histogram_tester;
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GURL(chrome::kChromeUINewTabURL)));
ui_test_utils::SendToOmniboxAndSubmit(browser(), "happy ");
histogram_tester.ExpectUniqueSample("InputMethod.Assistive.Disabled.Emoji",
DisabledReason::kUrlOrAppNotAllowed, 1);
}
IN_PROC_BROWSER_TEST_F(
NativeInputMethodEngineWithoutImeServiceTest,
OnLearnMoreButtonClickedOpensEmojiSuggestionSettingsPage) {
base::UserActionTester user_action_tester;
ui::ime::AssistiveWindowButton button;
button.id = ui::ime::ButtonId::kLearnMore;
button.window_type = ash::ime::AssistiveWindowType::kEmojiSuggestion;
engine_->AssistiveWindowButtonClicked(button);
EXPECT_EQ(1, user_action_tester.GetActionCount(
"ChromeOS.Settings.SmartInputs.EmojiSuggestions.Open"));
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
FiresOnInputMethodOptionsChangedEvent) {
{
base::Value::Dict settings;
// Add key will trigger event.
base::Value::Dict pinyin1;
pinyin1.Set("foo", true);
settings.SetByDottedPath("pinyin", std::move(pinyin1));
prefs_->Set(::prefs::kLanguageInputMethodSpecificSettings,
base::Value(std::move(settings)));
EXPECT_EQ(observer_->changed_engine_id(), "pinyin");
observer_->ClearChangedEngineId();
}
{
base::Value::Dict settings;
// Change key will trigger event.
base::Value::Dict pinyin2;
pinyin2.Set("foo", false);
settings.SetByDottedPath("pinyin", std::move(pinyin2));
prefs_->Set(::prefs::kLanguageInputMethodSpecificSettings,
base::Value(std::move(settings)));
EXPECT_EQ(observer_->changed_engine_id(), "pinyin");
}
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
DestroyProfile) {
EXPECT_NE(engine_->GetPrefChangeRegistrarForTesting(), nullptr);
profile_->MaybeSendDestroyedNotification();
EXPECT_EQ(engine_->GetPrefChangeRegistrarForTesting(), nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
HighlightsOnAutocorrectThenDismissesHighlight) {
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
ui::DummyTextInputClient text_input_client(ui::TEXT_INPUT_TYPE_TEXT);
SetFocus(&text_input_client);
engine_->OnAutocorrect(u"typed", u"corrected", 0);
// Input the corrected word.
helper.GetTextInputClient()->InsertText(
u"corrected ",
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(u"corrected ");
EXPECT_FALSE(text_input_client.GetAutocorrectRange().is_empty());
helper.GetTextInputClient()->InsertText(
u"aa",
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(u"corrected aa");
// Highlighting should only go away after inserting 3 characters.
EXPECT_FALSE(text_input_client.GetAutocorrectRange().is_empty());
helper.GetTextInputClient()->InsertText(
u"a",
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(u"corrected aaa");
EXPECT_TRUE(text_input_client.GetAutocorrectRange().is_empty());
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
ShowsAndHidesAutocorrectUndoWindow) {
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string prefix_text = u"corrected ";
helper.GetTextInputClient()->InsertText(
prefix_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(prefix_text);
engine_->OnAutocorrect(u"typed", u"corrected", 0);
auto* controller =
((AssistiveWindowController*)(IMEBridge::Get()
->GetAssistiveWindowHandler()));
EXPECT_FALSE(controller->GetUndoWindowForTesting());
// Move cursor back into the autocorrected word to show the window.
helper.GetTextInputClient()->ExtendSelectionAndDelete(1, 0);
helper.WaitForSurroundingTextChanged(u"corrected");
EXPECT_TRUE(controller->GetUndoWindowForTesting());
EXPECT_TRUE(controller->GetUndoWindowForTesting()->GetVisible());
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
RevertsAutocorrect) {
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string corrected_text = u"hello corrected world";
const std::u16string typed_text = u"hello typed world";
helper.GetTextInputClient()->InsertText(
corrected_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(corrected_text);
EXPECT_EQ(IMEBridge::Get()
->GetInputContextHandler()
->GetSurroundingTextInfo()
.surrounding_text,
corrected_text);
engine_->OnAutocorrect(u"typed", u"corrected", 6);
// Move cursor into the corrected word, sending VKEY_LEFT fails, so use JS.
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(content::ExecJs(
tab, "document.getElementById('text_id').setSelectionRange(8,8)"));
helper.WaitForSurroundingTextChanged(corrected_text, gfx::Range(8, 8));
engine_->get_autocorrect_manager_for_testing()->UndoAutocorrect();
helper.WaitForSurroundingTextChanged(typed_text);
EXPECT_EQ(IMEBridge::Get()
->GetInputContextHandler()
->GetSurroundingTextInfo()
.surrounding_text,
typed_text);
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
RevertsAutocorrectWithKeyboard) {
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string corrected_text = u"corrected";
const std::u16string typed_text = u"typed";
helper.GetTextInputClient()->InsertText(
corrected_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(corrected_text);
EXPECT_EQ(IMEBridge::Get()
->GetInputContextHandler()
->GetSurroundingTextInfo()
.surrounding_text,
corrected_text);
engine_->OnAutocorrect(u"typed", u"corrected", 0);
// Move cursor into the corrected word, sending VKEY_LEFT fails, so use JS.
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(content::ExecJs(
tab, "document.getElementById('text_id').setSelectionRange(2,2)"));
helper.WaitForSurroundingTextChanged(corrected_text, gfx::Range(2, 2));
DispatchKeyPress(ui::VKEY_UP, false);
DispatchKeyPress(ui::VKEY_RETURN, false);
helper.WaitForSurroundingTextChanged(typed_text);
EXPECT_EQ(IMEBridge::Get()
->GetInputContextHandler()
->GetSurroundingTextInfo()
.surrounding_text,
typed_text);
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
SendsAutocorrectMetricsforUnderline) {
base::HistogramTester histogram_tester;
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
const std::u16string corrected_text = u"corrected";
const std::u16string typed_text = u"typed";
helper.GetTextInputClient()->InsertText(
corrected_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
helper.WaitForSurroundingTextChanged(corrected_text);
EXPECT_EQ(IMEBridge::Get()
->GetInputContextHandler()
->GetSurroundingTextInfo()
.surrounding_text,
corrected_text);
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectWindowShown, 0);
engine_->OnAutocorrect(u"typed", u"corrected", 0);
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectUnderlined, 1);
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectWindowShown, 0);
// Move cursor into the corrected word, sending VKEY_LEFT fails, so use JS.
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(content::ExecJs(
tab, "document.getElementById('text_id').setSelectionRange(2,2)"));
helper.WaitForSurroundingTextChanged(corrected_text, gfx::Range(2, 2));
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectWindowShown, 1);
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectReverted, 0);
DispatchKeyPress(ui::VKEY_UP, false);
DispatchKeyPress(ui::VKEY_RETURN, false);
helper.WaitForSurroundingTextChanged(typed_text);
histogram_tester.ExpectBucketCount("InputMethod.Assistive.Coverage",
AssistiveType::kAutocorrectReverted, 1);
SetFocus(nullptr);
}
IN_PROC_BROWSER_TEST_F(NativeInputMethodEngineWithoutImeServiceTest,
ReplaceSurroundingTextPerformsAtomicInsertText) {
engine_->Enable(kEngineIdUs);
TextInputTestHelper helper(GetBrowserInputMethod());
SetUpTextInput(helper);
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(content::ExecJs(
tab, "document.getElementById('text_id').value = 'original'"));
ASSERT_TRUE(content::ExecJs(
tab, "document.getElementById('text_id').setSelectionRange(4,4)"));
helper.GetTextInputClient()->ExtendSelectionAndReplace(3, 2, u"replaced");
helper.WaitForSurroundingTextChanged(u"oreplacedal");
EXPECT_EQ(helper.GetElementInnerText("text_events", tab),
"replaced;insertText;false\n");
SetFocus(nullptr);
}
} // namespace input_method
} // namespace ash