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

// Copyright 2020 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/emoji_suggester.h"

#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/ash/input_method/input_method_engine.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"

namespace ash {
namespace input_method {
namespace {

using AssistiveSuggestion = ime::AssistiveSuggestion;
using AssistiveSuggestionMode = ime::AssistiveSuggestionMode;
using AssistiveSuggestionType = ime::AssistiveSuggestionType;

}  // namespace

ui::KeyEvent CreateKeyEventFromCode(const ui::DomCode& code) {
  return ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_UNKNOWN, code,
                      ui::EF_NONE, ui::DomKey::NONE, ui::EventTimeForNow());
}

const char kEmojiData[] = "happy,😀;😃;😄";
const int kContextId = 24601;

class TestSuggestionHandler : public SuggestionHandlerInterface {
 public:
  bool SetButtonHighlighted(int context_id,
                            const ui::ime::AssistiveWindowButton& button,
                            bool highlighted,
                            std::string* error) override {
    switch (button.id) {
      case ui::ime::ButtonId::kLearnMore:
        learn_more_button_highlighted_ = highlighted;
        return true;
      case ui::ime::ButtonId::kSuggestion:
        // If highlighted, needs to unhighlight previously highlighted button.
        if (currently_highlighted_index_ != INT_MAX && highlighted) {
          candidate_highlighted_[currently_highlighted_index_] = 0;
        }
        currently_highlighted_index_ =
            highlighted ? button.suggestion_index : INT_MAX;
        candidate_highlighted_[button.suggestion_index] = highlighted ? 1 : 0;
        return true;
      default:
        return false;
    }
  }

  bool SetAssistiveWindowProperties(
      int context_id,
      const AssistiveWindowProperties& assistive_window,
      std::string* error) override {
    context_id_ = context_id;
    candidate_highlighted_.clear();
    for (size_t i = 0; i < assistive_window.candidates.size(); i++) {
      candidate_highlighted_.push_back(0);
    }
    show_indices_ = assistive_window.show_indices;
    show_setting_link_ = assistive_window.show_setting_link;
    return true;
  }

  void VerifyShowIndices(bool show_indices) {
    EXPECT_EQ(show_indices_, show_indices);
  }

  void VerifyLearnMoreButtonHighlighted(const bool highlighted) {
    EXPECT_EQ(learn_more_button_highlighted_, highlighted);
  }

  void VerifyCandidateHighlighted(const int index, const bool highlighted) {
    int expect = highlighted ? 1 : 0;
    EXPECT_EQ(candidate_highlighted_[index], expect);
  }

  void VerifyShowSettingLink(const bool show_setting_link) {
    EXPECT_EQ(show_setting_link_, show_setting_link);
  }

  void VerifyContextId(const int context_id) {
    EXPECT_EQ(context_id_, context_id);
  }

  bool DismissSuggestion(int context_id, std::string* error) override {
    return false;
  }

  bool AcceptSuggestion(int context_id, std::string* error) override {
    return false;
  }

  void OnSuggestionsChanged(
      const std::vector<std::string>& suggestions) override {}

  void ClickButton(const ui::ime::AssistiveWindowButton& button) override {}

  bool AcceptSuggestionCandidate(int context_id,
                                 const std::u16string& candidate,
                                 size_t delete_previous_utf16_len,
                                 bool use_replace_surrounding_text,
                                 std::string* error) override {
    return false;
  }

  bool SetSuggestion(int context_id,
                     const ui::ime::SuggestionDetails& details,
                     std::string* error) override {
    return false;
  }

  void Announce(const std::u16string& message) override {}

 private:
  bool show_indices_ = false;
  bool show_setting_link_ = false;
  bool learn_more_button_highlighted_ = false;
  std::vector<int> candidate_highlighted_;
  size_t currently_highlighted_index_ = INT_MAX;
  int context_id_ = -1;
};

class EmojiSuggesterTest : public testing::Test {
 protected:
  void SetUp() override {
    engine_ = std::make_unique<TestSuggestionHandler>();
    profile_ = std::make_unique<TestingProfile>();
    emoji_suggester_ =
        std::make_unique<EmojiSuggester>(engine_.get(), profile_.get());
    emoji_suggester_->LoadEmojiMapForTesting(kEmojiData);
    chrome_keyboard_controller_client_ =
        ChromeKeyboardControllerClient::CreateForTest();
    chrome_keyboard_controller_client_->set_keyboard_visible_for_test(false);
    emoji_suggester_->OnFocus(kContextId);
  }

  SuggestionStatus Press(ui::DomCode code) {
    return emoji_suggester_->HandleKeyEvent(CreateKeyEventFromCode(code));
  }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<EmojiSuggester> emoji_suggester_;
  std::unique_ptr<TestSuggestionHandler> engine_;
  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<ChromeKeyboardControllerClient>
      chrome_keyboard_controller_client_;
};

TEST_F(EmojiSuggesterTest, SuggestWhenStringEndsWithSpace) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
}

TEST_F(EmojiSuggesterTest, SuggestWhenStringEndsWithSpaceInNewLine) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(
      u"oldline\nhappy ", gfx::Range(14)));
}

TEST_F(EmojiSuggesterTest, PassesContextIdToHandlerOnSuggestion) {
  emoji_suggester_->TrySuggestWithSurroundingText(u"happy ", gfx::Range(6));
  engine_->VerifyContextId(kContextId);
}

TEST_F(EmojiSuggesterTest, SuggestWhenStringStartsWithOpenBracket) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"(happy ",
                                                              gfx::Range(7)));
}

TEST_F(EmojiSuggesterTest, SuggestWhenStringEndsWithSpaceAndIsUppercase) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"HAPPY ",
                                                              gfx::Range(6)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestWhenStringEndsWithNewLine) {
  EXPECT_FALSE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy\n",
                                                               gfx::Range(6)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestWhenStringDoesNotEndWithSpace) {
  EXPECT_FALSE(
      emoji_suggester_->TrySuggestWithSurroundingText(u"happy", gfx::Range(5)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestOnWhenContainsCursorSelection) {
  EXPECT_FALSE(emoji_suggester_->TrySuggestWithSurroundingText(
      u"happy ", gfx::Range(6, 2)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestOnWhenNotAtEndOfText) {
  EXPECT_FALSE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                               gfx::Range(3)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestWhenWordNotInMap) {
  EXPECT_FALSE(
      emoji_suggester_->TrySuggestWithSurroundingText(u"hapy ", gfx::Range(5)));
}

TEST_F(EmojiSuggesterTest, DoNotSuggestAfterBlur) {
  emoji_suggester_->OnBlur();
  EXPECT_FALSE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                               gfx::Range(6)));
}

TEST_F(EmojiSuggesterTest, DoNotShowSuggestionWhenVirtualKeyboardEnabled) {
  chrome_keyboard_controller_client_->set_keyboard_visible_for_test(true);
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  EXPECT_FALSE(emoji_suggester_->HasSuggestions());
}

TEST_F(EmojiSuggesterTest, ReturnkBrowsingWhenPressingDown) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event = CreateKeyEventFromCode(ui::DomCode::ARROW_DOWN);
  EXPECT_EQ(SuggestionStatus::kBrowsing,
            emoji_suggester_->HandleKeyEvent(event));
}

TEST_F(EmojiSuggesterTest, ReturnkBrowsingWhenPressingUp) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event = CreateKeyEventFromCode(ui::DomCode::ARROW_UP);
  EXPECT_EQ(SuggestionStatus::kBrowsing,
            emoji_suggester_->HandleKeyEvent(event));
}

TEST_F(EmojiSuggesterTest, ReturnkDismissWhenPressingEsc) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event = CreateKeyEventFromCode(ui::DomCode::ESCAPE);
  EXPECT_EQ(SuggestionStatus::kDismiss,
            emoji_suggester_->HandleKeyEvent(event));
}

TEST_F(EmojiSuggesterTest, ReturnkNotHandledWhenPressDownThenValidNumber) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event1 = CreateKeyEventFromCode(ui::DomCode::ARROW_DOWN);
  emoji_suggester_->HandleKeyEvent(event1);
  ui::KeyEvent event2 = CreateKeyEventFromCode(ui::DomCode::DIGIT1);
  EXPECT_EQ(SuggestionStatus::kNotHandled,
            emoji_suggester_->HandleKeyEvent(event2));
}

TEST_F(EmojiSuggesterTest, ReturnkNotHandledWhenPressDownThenNotANumber) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event1 = CreateKeyEventFromCode(ui::DomCode::ARROW_DOWN);
  emoji_suggester_->HandleKeyEvent(event1);
  ui::KeyEvent event2 = CreateKeyEventFromCode(ui::DomCode::US_A);
  EXPECT_EQ(SuggestionStatus::kNotHandled,
            emoji_suggester_->HandleKeyEvent(event2));
}

TEST_F(EmojiSuggesterTest,
       ReturnkNotHandledWhenPressingEnterAndACandidateHasNotBeenChosen) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  ui::KeyEvent event = CreateKeyEventFromCode(ui::DomCode::ENTER);
  EXPECT_EQ(SuggestionStatus::kNotHandled,
            emoji_suggester_->HandleKeyEvent(event));
}

TEST_F(EmojiSuggesterTest,
       ReturnkAcceptWhenPressingEnterAndACandidateHasBeenChosenByPressingDown) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  // Press ui::DomCode::ARROW_DOWN to choose a candidate.
  ui::KeyEvent event1 = CreateKeyEventFromCode(ui::DomCode::ARROW_DOWN);
  emoji_suggester_->HandleKeyEvent(event1);
  ui::KeyEvent event2 = CreateKeyEventFromCode(ui::DomCode::ENTER);
  EXPECT_EQ(SuggestionStatus::kAccept,
            emoji_suggester_->HandleKeyEvent(event2));
}

TEST_F(EmojiSuggesterTest, HighlightFirstCandidateWhenPressingDown) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  Press(ui::DomCode::ARROW_DOWN);
  engine_->VerifyCandidateHighlighted(0, true);
}

TEST_F(EmojiSuggesterTest, HighlightButtonCorrectlyWhenPressingUp) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  // Go into the window.
  Press(ui::DomCode::ARROW_DOWN);

  // Press ui::DomCode::ARROW_UP to choose learn more button.
  Press(ui::DomCode::ARROW_UP);
  engine_->VerifyLearnMoreButtonHighlighted(true);

  // Press ui::DomCode::ARROW_UP to go through candidates;
  for (size_t i = emoji_suggester_->GetCandidatesSizeForTesting(); i > 0; i--) {
    Press(ui::DomCode::ARROW_UP);
    engine_->VerifyCandidateHighlighted(i - 1, true);
    engine_->VerifyLearnMoreButtonHighlighted(false);
    if (i != emoji_suggester_->GetCandidatesSizeForTesting()) {
      engine_->VerifyCandidateHighlighted(i, false);
    }
  }

  // Press ui::DomCode::ARROW_UP to go to learn more button from first
  // candidate.
  Press(ui::DomCode::ARROW_UP);
  engine_->VerifyLearnMoreButtonHighlighted(true);
}

TEST_F(EmojiSuggesterTest, HighlightButtonCorrectlyWhenPressingDown) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  // Press ui::DomCode::ARROW_DOWN to go through candidates.
  for (size_t i = 0; i < emoji_suggester_->GetCandidatesSizeForTesting(); i++) {
    Press(ui::DomCode::ARROW_DOWN);
    engine_->VerifyCandidateHighlighted(i, true);
    engine_->VerifyLearnMoreButtonHighlighted(false);
    if (i != 0) {
      engine_->VerifyCandidateHighlighted(i - 1, false);
    }
  }

  // Go to LearnMore Button
  Press(ui::DomCode::ARROW_DOWN);
  engine_->VerifyLearnMoreButtonHighlighted(true);
  engine_->VerifyCandidateHighlighted(
      emoji_suggester_->GetCandidatesSizeForTesting() - 1, false);

  // Go to first candidate
  Press(ui::DomCode::ARROW_DOWN);
  engine_->VerifyLearnMoreButtonHighlighted(false);
  engine_->VerifyCandidateHighlighted(0, true);
}

TEST_F(EmojiSuggesterTest,
       OpenSettingWhenPressingEnterAndLearnMoreButtonIsChosen) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  // Go into the window.
  Press(ui::DomCode::ARROW_DOWN);
  // Choose Learn More Button.
  Press(ui::DomCode::ARROW_UP);
  engine_->VerifyLearnMoreButtonHighlighted(true);

  EXPECT_EQ(Press(ui::DomCode::ENTER), SuggestionStatus::kOpenSettings);
}

TEST_F(EmojiSuggesterTest, DoesNotShowIndicesWhenFirstSuggesting) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  engine_->VerifyShowIndices(false);
}

TEST_F(EmojiSuggesterTest, DoesNotShowIndexAfterPressingDown) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  Press(ui::DomCode::ARROW_DOWN);

  engine_->VerifyShowIndices(false);
}

TEST_F(EmojiSuggesterTest, DoesNotShowIndicesAfterGettingSuggestionsTwice) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  engine_->VerifyShowIndices(false);
}

TEST_F(EmojiSuggesterTest,
       DoesNotShowIndicesAfterPressingDownThenGetNewSuggestions) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  Press(ui::DomCode::ARROW_DOWN);
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));

  engine_->VerifyShowIndices(false);
}

TEST_F(EmojiSuggesterTest, ShowSettingLinkCorrectly) {
  for (int i = 0; i < kEmojiSuggesterShowSettingMaxCount; i++) {
    emoji_suggester_->TrySuggestWithSurroundingText(u"happy ", gfx::Range(6));
    // Dismiss suggestion.
    Press(ui::DomCode::ESCAPE);
    engine_->VerifyShowSettingLink(true);
  }
  emoji_suggester_->TrySuggestWithSurroundingText(u"happy ", gfx::Range(6));
  engine_->VerifyShowSettingLink(false);
}

TEST_F(EmojiSuggesterTest, IsShowingSuggestionTrueWhenCandidatesAvailable) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  EXPECT_TRUE(emoji_suggester_->HasSuggestions());
}

TEST_F(EmojiSuggesterTest, IsShowingSuggestionFalseWhenCandidatesUnavailable) {
  EXPECT_FALSE(
      emoji_suggester_->TrySuggestWithSurroundingText(u"hapy", gfx::Range(4)));
  EXPECT_FALSE(emoji_suggester_->HasSuggestions());
}

TEST_F(EmojiSuggesterTest, GetSuggestionReturnsCandidatesWhenAvailable) {
  EXPECT_TRUE(emoji_suggester_->TrySuggestWithSurroundingText(u"happy ",
                                                              gfx::Range(6)));
  EXPECT_EQ(
      emoji_suggester_->GetSuggestions(),
      (std::vector<AssistiveSuggestion>{
          AssistiveSuggestion{.mode = AssistiveSuggestionMode::kPrediction,
                              .type = AssistiveSuggestionType::kAssistiveEmoji,
                              .text = "😀"},
          AssistiveSuggestion{.mode = AssistiveSuggestionMode::kPrediction,
                              .type = AssistiveSuggestionType::kAssistiveEmoji,
                              .text = "😃"},
          AssistiveSuggestion{.mode = AssistiveSuggestionMode::kPrediction,
                              .type = AssistiveSuggestionType::kAssistiveEmoji,
                              .text = "😄"},
      }));
}

TEST_F(EmojiSuggesterTest,
       GetSuggestionDoesNotReturnCandidatesWhenUnavailable) {
  EXPECT_FALSE(
      emoji_suggester_->TrySuggestWithSurroundingText(u"hapy", gfx::Range(4)));
  EXPECT_TRUE(emoji_suggester_->GetSuggestions().empty());
}
}  // namespace input_method
}  // namespace ash