chromium/chromeos/components/quick_answers/search_result_parsers/definition_result_parser_unittest.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/components/quick_answers/search_result_parsers/definition_result_parser.h"

#include <memory>
#include <string>

#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/components/quick_answers/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/color/color_id.h"

namespace quick_answers {
namespace {
constexpr char kPhoneticsAudioUrlWithoutProtocol[] = "//example.com/audio";
constexpr char kPhoneticsAudioUrlWithProtocol[] = "https://example.com/audio";

using base::Value;
}  // namespace

class DefinitionResultParserTest : public testing::Test {
 public:
  DefinitionResultParserTest()
      : parser_(std::make_unique<DefinitionResultParser>()) {}

  DefinitionResultParserTest(const DefinitionResultParserTest&) = delete;
  DefinitionResultParserTest& operator=(const DefinitionResultParserTest&) =
      delete;

 protected:
  Value::Dict BuildDictionaryResult(
      const std::string& query_term,
      const std::string& phonetic_str,
      const std::string& definition,
      const std::string& word_class,
      const std::string& sample_sentence = std::string(),
      const std::vector<std::string>& synonyms_list = {},
      const std::vector<std::string>& subsenses_list = {}) {
    Value::Dict result;

    if (!query_term.empty()) {
      result.SetByDottedPath("dictionaryResult.queryTerm", query_term);
    }

    // Build definition entry.
    Value::List entries;
    Value::Dict entry;

    entry.Set("locale", "en");

    // Build phonetics.
    if (!phonetic_str.empty()) {
      Value::List phonetics;
      Value::Dict phonetic;
      phonetic.Set("text", phonetic_str);
      phonetic.Set("oxfordAudio", kPhoneticsAudioUrlWithoutProtocol);
      phonetics.Append(std::move(phonetic));
      entry.Set("phonetics", std::move(phonetics));
    }

    // Build senses.
    if (!definition.empty()) {
      Value::List sense_families;
      Value::Dict sense_family;
      Value::List senses;
      Value::Dict sense;

      sense.SetByDottedPath("definition.text", definition);
      if (!sample_sentence.empty()) {
        Value::List example_groups;
        Value::Dict example_group;
        Value::List examples;

        examples.Append(sample_sentence);
        example_group.Set("examples", std::move(examples));
        example_groups.Append(std::move(example_group));
        sense.Set("exampleGroups", std::move(example_groups));
      }
      if (!synonyms_list.empty()) {
        Value::List thesaurus_entries;
        Value::Dict thesaurus_entry;
        Value::List synonyms;
        Value::Dict synonym;
        Value::List nyms;

        for (std::string synonym_term : synonyms_list) {
          Value::Dict nym;
          nym.Set("nym", synonym_term);
          nyms.Append(std::move(nym));
        }
        synonym.Set("nyms", std::move(nyms));
        synonyms.Append(std::move(synonym));
        thesaurus_entry.Set("synonyms", std::move(synonyms));
        thesaurus_entries.Append(std::move(thesaurus_entry));
        sense.Set("thesaurusEntries", std::move(thesaurus_entries));
      }
      if (!subsenses_list.empty()) {
        Value::List subsenses;

        for (std::string subsense_term : subsenses_list) {
          Value::Dict subsense;
          subsense.SetByDottedPath("definition.text", subsense_term);
          subsenses.Append(std::move(subsense));
        }
        sense.Set("subsenses", std::move(subsenses));
      }
      senses.Append(std::move(sense));
      sense_family.Set("senses", std::move(senses));

      if (!word_class.empty()) {
        Value::List parts_of_speech;
        Value::Dict part_of_speech;
        part_of_speech.Set("value", word_class);
        parts_of_speech.Append(std::move(part_of_speech));
        sense_family.Set("partsOfSpeech", std::move(parts_of_speech));
      }
      sense_families.Append(std::move(sense_family));
      entry.Set("senseFamilies", std::move(sense_families));
    }

    entries.Append(std::move(entry));
    result.SetByDottedPath("dictionaryResult.entries", std::move(entries));

    return result;
  }

  void SetHeadWord(Value::Dict& result, const std::string& headword) {
    (*result.FindListByDottedPath("dictionaryResult.entries"))[0].GetDict().Set(
        "headword", headword);
  }

  std::unique_ptr<DefinitionResultParser> parser_;
};

TEST_F(DefinitionResultParserTest, Success) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"unfathomable", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"adjective");
  QuickAnswer quick_answer;
  EXPECT_TRUE(parser_->Parse(result, &quick_answer));

  const auto& expected_title = "unfathomable · /ˌənˈfaT͟Həməb(ə)/";
  const auto& expected_answer =
      "incapable of being fully explored or understood.";
  EXPECT_EQ(ResultType::kDefinitionResult, quick_answer.result_type);

  EXPECT_EQ(1u, quick_answer.title.size());
  EXPECT_EQ(1u, quick_answer.first_answer_row.size());
  auto* answer =
      static_cast<QuickAnswerText*>(quick_answer.first_answer_row[0].get());
  EXPECT_EQ(expected_answer,
            GetQuickAnswerTextForTesting(quick_answer.first_answer_row));
  EXPECT_EQ(ui::kColorLabelForegroundSecondary, answer->color_id);
  auto* title = static_cast<QuickAnswerText*>(quick_answer.title[0].get());
  EXPECT_EQ(expected_title, GetQuickAnswerTextForTesting(quick_answer.title));
  EXPECT_EQ(ui::kColorLabelForeground, title->color_id);

  // Expectations for `StructuredResult`.
  std::unique_ptr<StructuredResult> structured_result =
      parser_->ParseInStructuredResult(result);
  ASSERT_TRUE(structured_result);
  ASSERT_TRUE(structured_result->definition_result);

  DefinitionResult* definition_result =
      structured_result->definition_result.get();
  EXPECT_EQ(definition_result->word, "unfathomable");
  EXPECT_EQ(definition_result->word_class, "adjective");

  // `PhoneticsInfo::query_text` is headword. It's a query text for TTS. We
  // should not expect this test case response from the server as either
  // `query_text` or `phonetics_audio` should be filled.
  EXPECT_TRUE(definition_result->phonetics_info.query_text.empty());
  EXPECT_EQ(definition_result->phonetics_info.text, "ˌənˈfaT͟Həməb(ə)");
  EXPECT_EQ(definition_result->phonetics_info.locale, "en");
  EXPECT_EQ(definition_result->phonetics_info.phonetics_audio,
            GURL(kPhoneticsAudioUrlWithProtocol));
  EXPECT_FALSE(definition_result->phonetics_info.tts_audio_enabled);

  EXPECT_EQ(definition_result->sense.definition, expected_answer);
}

TEST_F(DefinitionResultParserTest, SuccessWithRichCardInfo) {
  const std::vector<std::string> synonyms_list = {"fine", "nice", "minute",
                                                  "precise"};
  const std::vector<std::string> subsenses_list = {
      "(of a mixture or effect) delicately complex and understated.",
      "making use of clever and indirect methods to achieve something.",
      "capable of making fine distinctions.",
      "arranged in an ingenious and elaborate way."};
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"subtle", /*phonetic_str=*/"ˈsəd(ə)l",
      /*definition=*/
      "(especially of a change or distinction) so delicate or precise as to be "
      "difficult to analyze or describe.",
      /*word_class=*/"adjective",
      /*sample_sentence=*/"his language expresses rich and subtle meanings",
      synonyms_list, subsenses_list);
  QuickAnswer quick_answer;
  EXPECT_TRUE(parser_->Parse(result, &quick_answer));

  EXPECT_EQ(ResultType::kDefinitionResult, quick_answer.result_type);

  // Expectations for `StructuredResult`.
  std::unique_ptr<StructuredResult> structured_result =
      parser_->ParseInStructuredResult(result);
  ASSERT_TRUE(structured_result);
  ASSERT_TRUE(structured_result->definition_result);

  DefinitionResult* definition_result =
      structured_result->definition_result.get();
  EXPECT_EQ(definition_result->word, "subtle");
  EXPECT_EQ(definition_result->word_class, "adjective");

  // `PhoneticsInfo::query_text` is headword. It's a query text for TTS. We
  // should not expect this test case response from the server as either
  // `query_text` or `phonetics_audio` should be filled.
  EXPECT_TRUE(definition_result->phonetics_info.query_text.empty());
  EXPECT_EQ(definition_result->phonetics_info.text, "ˈsəd(ə)l");
  EXPECT_EQ(definition_result->phonetics_info.locale, "en");
  EXPECT_EQ(definition_result->phonetics_info.phonetics_audio,
            GURL(kPhoneticsAudioUrlWithProtocol));
  EXPECT_FALSE(definition_result->phonetics_info.tts_audio_enabled);

  EXPECT_EQ(
      definition_result->sense.definition,
      "(especially of a change or distinction) so delicate or precise as to be "
      "difficult to analyze or describe.");

  // Rich card specific `StructuredResult` fields.
  EXPECT_TRUE(definition_result->sense.sample_sentence.has_value());
  EXPECT_EQ(definition_result->sense.sample_sentence,
            "his language expresses rich and subtle meanings");

  EXPECT_TRUE(definition_result->sense.synonyms_list.has_value());
  const std::vector<std::string> expected_synonyms_list = {"fine", "nice",
                                                           "minute"};
  EXPECT_EQ(definition_result->sense.synonyms_list, expected_synonyms_list);

  EXPECT_TRUE(definition_result->subsenses_list.has_value());
  const std::vector<std::string> expected_subsenses_list = {
      "(of a mixture or effect) delicately complex and understated.",
      "making use of clever and indirect methods to achieve something.",
      "capable of making fine distinctions."};
  std::vector<std::string> actual_subsenses_list = {};
  for (quick_answers::Sense subsense :
       definition_result->subsenses_list.value()) {
    actual_subsenses_list.push_back(subsense.definition);
  }
  EXPECT_EQ(actual_subsenses_list, expected_subsenses_list);
}

TEST_F(DefinitionResultParserTest, EmptyValue) {
  Value::Dict result;
  QuickAnswer quick_answer;
  EXPECT_FALSE(parser_->Parse(result, &quick_answer));
}

TEST_F(DefinitionResultParserTest, NoQueryTerm) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"adjective");
  QuickAnswer quick_answer;
  EXPECT_FALSE(parser_->Parse(result, &quick_answer));
}

TEST_F(DefinitionResultParserTest, NoQueryTermShouldFallbackToHeadword) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"adjective");
  SetHeadWord(result, "unfathomable");
  QuickAnswer quick_answer;
  EXPECT_TRUE(parser_->Parse(result, &quick_answer));

  const auto& expected_title = "unfathomable · /ˌənˈfaT͟Həməb(ə)/";
  EXPECT_EQ(expected_title, GetQuickAnswerTextForTesting(quick_answer.title));
}

TEST_F(DefinitionResultParserTest, ShouldPrioritizeQueryTerm) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"Unfathomable", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"adjective");
  SetHeadWord(result, "Unfathomable");
  QuickAnswer quick_answer;
  EXPECT_TRUE(parser_->Parse(result, &quick_answer));

  const auto& expected_title = "Unfathomable · /ˌənˈfaT͟Həməb(ə)/";
  EXPECT_EQ(expected_title, GetQuickAnswerTextForTesting(quick_answer.title));
}

TEST_F(DefinitionResultParserTest, NoPhonetic) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"unfathomable", /*phonetic_str=*/"",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"adjective");
  QuickAnswer quick_answer;
  EXPECT_TRUE(parser_->Parse(result, &quick_answer));

  const auto& expected_title = "unfathomable";
  const auto& expected_answer =
      "incapable of being fully explored or understood.";
  EXPECT_EQ(ResultType::kDefinitionResult, quick_answer.result_type);
  EXPECT_EQ(expected_answer,
            GetQuickAnswerTextForTesting(quick_answer.first_answer_row));
  EXPECT_EQ(expected_title, GetQuickAnswerTextForTesting(quick_answer.title));
}

TEST_F(DefinitionResultParserTest, NoDefinition) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"unfathomable", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)l",
      /*definition=*/"", /*word_class=*/"adjective");
  QuickAnswer quick_answer;
  EXPECT_FALSE(parser_->Parse(result, &quick_answer));
}

TEST_F(DefinitionResultParserTest, NoWordClass) {
  Value::Dict result = BuildDictionaryResult(
      /*query_term=*/"unfathomable", /*phonetic_str=*/"ˌənˈfaT͟Həməb(ə)l",
      /*definition=*/"incapable of being fully explored or understood.",
      /*word_class=*/"");
  QuickAnswer quick_answer;
  EXPECT_FALSE(parser_->Parse(result, &quick_answer));
}

}  // namespace quick_answers