| // Copyright 2014 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/ash/input_method/input_method_engine.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/histogram_samples.h" |
| #include "base/metrics/statistics_recorder.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/ash/input_method/input_method_configuration.h" |
| #include "chrome/browser/ash/input_method/stub_input_method_engine_observer.h" |
| #include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client_test_helper.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/ime/ash/extension_ime_util.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/base/ime/ash/mock_component_extension_ime_manager_delegate.h" |
| #include "ui/base/ime/ash/mock_ime_candidate_window_handler.h" |
| #include "ui/base/ime/ash/mock_ime_input_context_handler.h" |
| #include "ui/base/ime/ash/mock_input_method_manager_impl.h" |
| #include "ui/base/ime/ash/text_input_method.h" |
| #include "ui/base/ime/text_input_flags.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace ash::input_method { |
| |
| namespace { |
| |
| using ::testing::SizeIs; |
| |
| const char kTestExtensionId[] = "mppnpdlheglhdfmldimlhpnegondlapf"; |
| const char kTestExtensionId2[] = "dmpipdbjkoajgdeppkffbjhngfckdloi"; |
| const char kTestImeComponentId[] = "test_engine_id"; |
| const char kFakeMozcComponentId[] = "nacl_mozc_test"; |
| |
| enum CallsBitmap { |
| NONE = 0U, |
| ACTIVATE = 1U, |
| DEACTIVATED = 2U, |
| ONFOCUS = 4U, |
| ONBLUR = 8U, |
| RESET = 16U |
| }; |
| |
| void InitInputMethod(const std::string& engine_id) { |
| auto delegate = std::make_unique<MockComponentExtensionIMEManagerDelegate>(); |
| |
| ComponentExtensionIME ext1; |
| ext1.id = kTestExtensionId; |
| |
| ComponentExtensionEngine ext1_engine1; |
| ext1_engine1.engine_id = engine_id; |
| ext1_engine1.language_codes.emplace_back("en-US"); |
| ext1_engine1.layout = "us"; |
| ext1.engines.push_back(ext1_engine1); |
| |
| std::vector<ComponentExtensionIME> ime_list; |
| ime_list.push_back(ext1); |
| delegate->set_ime_list(ime_list); |
| |
| auto comp_ime_manager = |
| std::make_unique<ComponentExtensionIMEManager>(std::move(delegate)); |
| |
| auto* manager = new MockInputMethodManagerImpl; |
| manager->SetComponentExtensionIMEManager(std::move(comp_ime_manager)); |
| InitializeForTesting(manager); |
| } |
| |
| class TestObserver : public StubInputMethodEngineObserver { |
| public: |
| TestObserver() = default; |
| TestObserver(const TestObserver&) = delete; |
| TestObserver& operator=(const TestObserver&) = delete; |
| ~TestObserver() override = default; |
| |
| void OnActivate(const std::string& engine_id) override { |
| calls_bitmap_ |= ACTIVATE; |
| engine_id_ = engine_id; |
| } |
| void OnDeactivated(const std::string& engine_id) override { |
| calls_bitmap_ |= DEACTIVATED; |
| engine_id_ = engine_id; |
| } |
| void OnFocus(const std::string& engine_id, |
| int context_id, |
| const TextInputMethod::InputContext& context) override { |
| calls_bitmap_ |= ONFOCUS; |
| } |
| void OnBlur(const std::string& engine_id, int context_id) override { |
| calls_bitmap_ |= ONBLUR; |
| } |
| void OnKeyEvent(const std::string& engine_id, |
| const ui::KeyEvent& event, |
| TextInputMethod::KeyEventDoneCallback callback) override { |
| std::move(callback).Run(ui::ime::KeyEventHandledState::kHandledByIME); |
| } |
| void OnReset(const std::string& engine_id) override { |
| calls_bitmap_ |= RESET; |
| engine_id_ = engine_id; |
| } |
| |
| unsigned char GetCallsBitmapAndReset() { |
| unsigned char ret = calls_bitmap_; |
| calls_bitmap_ = NONE; |
| return ret; |
| } |
| |
| std::string GetEngineIdAndReset() { |
| std::string engine_id{engine_id_}; |
| engine_id_.clear(); |
| return engine_id; |
| } |
| |
| private: |
| unsigned char calls_bitmap_ = 0; |
| std::string engine_id_; |
| }; |
| |
| class InputMethodEngineTest : public testing::Test { |
| public: |
| InputMethodEngineTest() : InputMethodEngineTest(kTestImeComponentId) {} |
| |
| explicit InputMethodEngineTest(const std::string& engine_id) |
| : observer_(nullptr), input_view_("inputview.html") { |
| languages_.emplace_back("en-US"); |
| layouts_.emplace_back("us"); |
| InitInputMethod(engine_id); |
| mock_ime_input_context_handler_ = |
| std::make_unique<MockIMEInputContextHandler>(); |
| IMEBridge::Get()->SetInputContextHandler( |
| mock_ime_input_context_handler_.get()); |
| |
| chrome_keyboard_controller_client_test_helper_ = |
| ChromeKeyboardControllerClientTestHelper::InitializeWithFake(); |
| } |
| |
| InputMethodEngineTest(const InputMethodEngineTest&) = delete; |
| InputMethodEngineTest& operator=(const InputMethodEngineTest&) = delete; |
| |
| ~InputMethodEngineTest() override { |
| IMEBridge::Get()->SetInputContextHandler(nullptr); |
| // |observer_| will be deleted in engine_.reset(). |
| observer_.ExtractAsDangling(); |
| engine_.reset(); |
| chrome_keyboard_controller_client_test_helper_.reset(); |
| Shutdown(); |
| } |
| |
| protected: |
| void CreateEngine(bool allowlisted) { |
| engine_ = std::make_unique<InputMethodEngine>(); |
| std::unique_ptr<InputMethodEngineObserver> observer = |
| std::make_unique<TestObserver>(); |
| observer_ = static_cast<TestObserver*>(observer.get()); |
| engine_->Initialize(std::move(observer), |
| allowlisted ? kTestExtensionId : kTestExtensionId2, |
| nullptr); |
| } |
| |
| void Focus(ui::TextInputType input_type) { |
| TextInputMethod::InputContext input_context(input_type); |
| engine_->Focus(input_context); |
| IMEBridge::Get()->SetCurrentInputContext(input_context); |
| } |
| |
| std::unique_ptr<InputMethodEngine> engine_; |
| |
| raw_ptr<TestObserver> observer_; |
| std::vector<std::string> languages_; |
| std::vector<std::string> layouts_; |
| GURL options_page_; |
| GURL input_view_; |
| |
| content::BrowserTaskEnvironment task_environment_; |
| std::unique_ptr<MockIMEInputContextHandler> mock_ime_input_context_handler_; |
| std::unique_ptr<ChromeKeyboardControllerClientTestHelper> |
| chrome_keyboard_controller_client_test_helper_; |
| }; |
| |
| } // namespace |
| |
| TEST_F(InputMethodEngineTest, TestSwitching) { |
| CreateEngine(false); |
| // Enable/disable with focus. |
| Focus(ui::TEXT_INPUT_TYPE_URL); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| // Enable/disable without focus. |
| engine_->Blur(); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| // Focus change when enabled. |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Blur(); |
| EXPECT_EQ(ONBLUR, observer_->GetCallsBitmapAndReset()); |
| // Focus change when disabled. |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| engine_->Blur(); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| } |
| |
| TEST_F(InputMethodEngineTest, TestSwitching_Password_3rd_Party) { |
| CreateEngine(false); |
| // Enable/disable with focus. |
| Focus(ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| // Focus change when enabled. |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Blur(); |
| EXPECT_EQ(ONBLUR, observer_->GetCallsBitmapAndReset()); |
| Focus(ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_EQ(ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| } |
| |
| TEST_F(InputMethodEngineTest, TestSwitching_Password_Allowlisted) { |
| CreateEngine(true); |
| // Enable/disable with focus. |
| Focus(ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_EQ(NONE, observer_->GetCallsBitmapAndReset()); |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| // Focus change when enabled. |
| engine_->Enable(kTestImeComponentId); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| engine_->Blur(); |
| EXPECT_EQ(ONBLUR, observer_->GetCallsBitmapAndReset()); |
| Focus(ui::TEXT_INPUT_TYPE_PASSWORD); |
| EXPECT_EQ(ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| engine_->Disable(); |
| EXPECT_EQ(DEACTIVATED, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| } |
| |
| // Tests input.ime.onReset API. |
| TEST_F(InputMethodEngineTest, TestReset) { |
| CreateEngine(false); |
| // Enables the extension with focus. |
| engine_->Enable(kTestImeComponentId); |
| Focus(ui::TEXT_INPUT_TYPE_URL); |
| EXPECT_EQ(ACTIVATE | ONFOCUS, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| |
| // Resets the engine. |
| engine_->Reset(); |
| EXPECT_EQ(RESET, observer_->GetCallsBitmapAndReset()); |
| EXPECT_EQ(kTestImeComponentId, observer_->GetEngineIdAndReset()); |
| } |
| |
| TEST_F(InputMethodEngineTest, TestHistograms) { |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kTestImeComponentId); |
| std::vector<InputMethodEngine::SegmentInfo> segments; |
| int context = engine_->GetContextIdForTesting(); |
| std::string error; |
| base::HistogramTester histograms; |
| engine_->SetComposition(context, "test", 0, 0, 0, segments, nullptr); |
| engine_->CommitText(context, u"input", &error); |
| engine_->SetComposition(context, "test", 0, 0, 0, segments, nullptr); |
| engine_->CommitText(context, |
| u"ä½ å¥½", // 2 UTF-16 code units |
| &error); |
| engine_->SetComposition(context, "test", 0, 0, 0, segments, nullptr); |
| engine_->CommitText(context, u"inputä½ å¥½", &error); |
| // This one shouldn't be counted because there was no composition. |
| engine_->CommitText(context, u"abc", &error); |
| histograms.ExpectTotalCount("InputMethod.CommitLength", 4); |
| histograms.ExpectBucketCount("InputMethod.CommitLength", 5, 1); |
| histograms.ExpectBucketCount("InputMethod.CommitLength", 2, 1); |
| histograms.ExpectBucketCount("InputMethod.CommitLength", 7, 1); |
| histograms.ExpectBucketCount("InputMethod.CommitLength", 3, 1); |
| } |
| |
| TEST_F(InputMethodEngineTest, TestInvalidCompositionReturnsFalse) { |
| CreateEngine(true); |
| std::string error; |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kTestImeComponentId); |
| std::vector<InputMethodEngine::SegmentInfo> segments; |
| int context = engine_->GetContextIdForTesting(); |
| EXPECT_EQ(engine_->SetComposition(context, "test", 0, 0, 0, segments, &error), |
| true); |
| EXPECT_EQ( |
| engine_->SetComposition(context, "test", -1, 0, 0, segments, &error), |
| false); |
| EXPECT_EQ( |
| engine_->SetComposition(context, "test", 0, -1, 0, segments, &error), |
| false); |
| EXPECT_EQ( |
| engine_->SetComposition(context, "test", 0, 0, -1, segments, &error), |
| false); |
| // Still return false if multiple values set as negative |
| EXPECT_EQ( |
| engine_->SetComposition(context, "test", -12, 0, -1, segments, &error), |
| false); |
| EXPECT_EQ( |
| engine_->SetComposition(context, "test", -1, -1, -1, segments, &error), |
| false); |
| EXPECT_EQ(engine_->SetComposition(context, "test", 0, 6, 0, segments, &error), |
| false); |
| } |
| |
| // See https://crbug.com/980437. |
| TEST_F(InputMethodEngineTest, TestDisableAfterSetCompositionRange) { |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kTestImeComponentId); |
| |
| const int context = engine_->GetContextIdForTesting(); |
| |
| std::string error; |
| engine_->CommitText(context, u"text", &error); |
| EXPECT_EQ("", error); |
| EXPECT_EQ(1, mock_ime_input_context_handler_->commit_text_call_count()); |
| EXPECT_EQ(u"text", mock_ime_input_context_handler_->last_commit_text()); |
| |
| // Change composition range to include "text". |
| engine_->InputMethodEngine::SetCompositionRange(context, 0, 4, {}, &error); |
| EXPECT_EQ("", error); |
| |
| // Disable to commit |
| engine_->Disable(); |
| |
| EXPECT_EQ("", error); |
| EXPECT_EQ(2, mock_ime_input_context_handler_->commit_text_call_count()); |
| EXPECT_EQ(u"text", mock_ime_input_context_handler_->last_commit_text()); |
| } |
| |
| TEST_F(InputMethodEngineTest, KeyEventHandledRecordsLatencyHistogram) { |
| CreateEngine(true); |
| base::HistogramTester histogram_tester; |
| |
| histogram_tester.ExpectTotalCount("InputMethod.KeyEventLatency", 0); |
| |
| const ui::KeyEvent event(ui::EventType::kKeyPressed, ui::VKEY_A, |
| ui::DomCode::US_A, 0, ui::DomKey::FromCharacter('a'), |
| ui::EventTimeForNow()); |
| engine_->ProcessKeyEvent(event, base::DoNothing()); |
| |
| histogram_tester.ExpectTotalCount("InputMethod.KeyEventLatency", 1); |
| } |
| |
| TEST_F(InputMethodEngineTest, AcceptSuggestionCandidateCommitsCandidate) { |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kTestImeComponentId); |
| |
| const int context = engine_->GetContextIdForTesting(); |
| |
| std::string error; |
| engine_->AcceptSuggestionCandidate(context, u"suggestion", 0, |
| &error); |
| |
| EXPECT_EQ("", error); |
| EXPECT_EQ( |
| 0, mock_ime_input_context_handler_->delete_surrounding_text_call_count()); |
| EXPECT_EQ(u"suggestion", mock_ime_input_context_handler_->last_commit_text()); |
| } |
| |
| TEST_F(InputMethodEngineTest, |
| AcceptSuggestionCandidateDeletesSurroundingAndCommitsCandidate) { |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kTestImeComponentId); |
| |
| const int context = engine_->GetContextIdForTesting(); |
| |
| std::string error; |
| engine_->CommitText(context, u"text", &error); |
| engine_->AcceptSuggestionCandidate(context, u"suggestion", 1, |
| &error); |
| |
| EXPECT_EQ("", error); |
| EXPECT_EQ( |
| 1, mock_ime_input_context_handler_->delete_surrounding_text_call_count()); |
| auto deleteSurroundingTextArg = |
| mock_ime_input_context_handler_->last_delete_surrounding_text_arg(); |
| EXPECT_EQ(deleteSurroundingTextArg.num_char16s_before_cursor, 1u); |
| EXPECT_EQ(deleteSurroundingTextArg.num_char16s_after_cursor, 0u); |
| EXPECT_EQ(u"suggestion", mock_ime_input_context_handler_->last_commit_text()); |
| } |
| |
| class InputMethodEngineWithJpIdTest : public InputMethodEngineTest { |
| public: |
| InputMethodEngineWithJpIdTest() |
| : InputMethodEngineTest(kFakeMozcComponentId) {} |
| }; |
| |
| TEST_F(InputMethodEngineWithJpIdTest, SetCandidatesUpdatesUserSelecting) { |
| auto mock_cw_handler = std::make_unique<MockIMECandidateWindowHandler>(); |
| IMEBridge::Get()->SetCandidateWindowHandler(mock_cw_handler.get()); |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kFakeMozcComponentId); |
| |
| std::vector<InputMethodEngine::Candidate> candidates(2); |
| candidates[0].id = 0; |
| candidates[1].id = 1; |
| std::string error; |
| ASSERT_TRUE(engine_->SetCandidateWindowVisible(true, &error)); |
| ASSERT_TRUE(engine_->SetCandidates(engine_->GetContextIdForTesting(), |
| candidates, &error)); |
| |
| EXPECT_EQ("", error); |
| const ui::CandidateWindow& candidate_window = |
| mock_cw_handler->last_update_lookup_table_arg().lookup_table; |
| EXPECT_THAT(candidate_window.candidates(), SizeIs(2)); |
| EXPECT_FALSE(candidate_window.is_user_selecting()); |
| } |
| |
| TEST_F(InputMethodEngineWithJpIdTest, |
| SetCandidatesWithLabelEnableUserSelecting) { |
| auto mock_cw_handler = std::make_unique<MockIMECandidateWindowHandler>(); |
| IMEBridge::Get()->SetCandidateWindowHandler(mock_cw_handler.get()); |
| CreateEngine(true); |
| Focus(ui::TEXT_INPUT_TYPE_TEXT); |
| engine_->Enable(kFakeMozcComponentId); |
| |
| std::vector<InputMethodEngine::Candidate> candidates(2); |
| candidates[0].id = 0; |
| candidates[0].label = "L1"; |
| candidates[1].id = 1; |
| candidates[1].label = "L2"; |
| std::string error; |
| ASSERT_TRUE(engine_->SetCandidateWindowVisible(true, &error)); |
| ASSERT_TRUE(engine_->SetCandidates(engine_->GetContextIdForTesting(), |
| candidates, &error)); |
| |
| EXPECT_EQ("", error); |
| const ui::CandidateWindow& candidate_window = |
| mock_cw_handler->last_update_lookup_table_arg().lookup_table; |
| EXPECT_THAT(candidate_window.candidates(), SizeIs(2)); |
| EXPECT_TRUE(candidate_window.is_user_selecting()); |
| } |
| |
| } // namespace ash::input_method |