chromium/chrome/browser/ash/input_method/input_method_engine_unittest.cc

// 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,
                                     /*use_replace_surrounding_text=*/false,
                                     &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,
       AcceptSuggestionCandidateReplacesSurroundingText) {
  CreateEngine(true);
  Focus(ui::TEXT_INPUT_TYPE_TEXT);
  engine_->Enable(kTestImeComponentId);

  const int context = engine_->GetContextIdForTesting();

  std::string error;
  engine_->AcceptSuggestionCandidate(context, u"suggestion", 3,
                                     /*use_replace_surrounding_text=*/true,
                                     &error);

  EXPECT_EQ("", error);
  const auto& arg =
      mock_ime_input_context_handler_->last_replace_surrounding_text_arg();
  EXPECT_EQ(3u, arg.length_before_selection);
  EXPECT_EQ(0u, arg.length_after_selection);
  EXPECT_EQ(u"suggestion", arg.replacement_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,
                                     /*use_replace_surrounding_text=*/false,
                                     &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