chromium/chrome/browser/speech/tts_crosapi_util_unittest.cc

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

#include <set>

#include "base/unguessable_token.h"
#include "base/values.h"
#include "chrome/browser/speech/extension_api/tts_extension_api_constants.h"
#include "chrome/browser/speech/tts_crosapi_util.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/crosapi/mojom/tts.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/tts_controller.h"
#include "content/public/browser/tts_utterance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

const char kText[] = "hello, world";
const char kVoiceName[] = "Alice";
const char kLang[] = "en-GB";
const int kSrcId = 12345;
const char kSrcUrl[] = "http://test.com";
const char kEngineId[] = "test_engine_id";
const double kRate = 1.0f;
const double kPitch = 0.5f;
const double kVolume = 0.9f;
const base::UnguessableToken kBrowerContextId =
    base::UnguessableToken::Create();

bool EventTypesMatches(
    const std::set<content::TtsEventType>& tts_event_types_in,
    const std::set<content::TtsEventType>& tts_event_types_out) {
  if (tts_event_types_in.size() != tts_event_types_out.size())
    return false;

  for (const auto& event_type : tts_event_types_in) {
    if (tts_event_types_out.find(event_type) == tts_event_types_out.end())
      return false;
  }

  return true;
}

}  // namespace

class TtsUtteranceMojomTest : public testing::Test {
 public:
  TtsUtteranceMojomTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
        rvh_test_enabler_(new content::RenderViewHostTestEnabler()) {}

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

  void SetUp() override {
    testing::Test::SetUp();

    testing_profile_ = TestingProfile::Builder().Build();
    web_contents_ = CreateTestWebContents();
  }

 protected:
  content::BrowserContext* browser_context() { return testing_profile_.get(); }
  content::WebContents* GetWebContents() { return web_contents_.get(); }

 private:
  std::unique_ptr<content::WebContents> CreateTestWebContents() {
    auto site_instance = content::SiteInstance::Create(browser_context());
    return content::WebContentsTester::CreateTestWebContents(
        browser_context(), std::move(site_instance));
  }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
  std::unique_ptr<TestingProfile> testing_profile_;
  std::unique_ptr<content::WebContents> web_contents_;
};

// Test that every field in content::TtsUtterance in correctly converted
// with a round trip conversion to and from crosapi::mojom::TtsUtterance.
TEST_F(TtsUtteranceMojomTest, RoundTripWithoutOptions) {
  std::unique_ptr<content::TtsUtterance> in_utterance =
      content::TtsUtterance::Create();
  in_utterance->SetText(kText);
  in_utterance->SetVoiceName(kVoiceName);
  in_utterance->SetLang(kLang);
  in_utterance->SetSrcId(kSrcId);
  in_utterance->SetSrcUrl(GURL(kSrcUrl));
  in_utterance->SetEngineId(kEngineId);
  in_utterance->SetContinuousParameters(kRate, kPitch, kVolume);
  in_utterance->SetShouldClearQueue(true);

  std::set<content::TtsEventType> required_event_types;
  required_event_types.insert(content::TTS_EVENT_START);
  required_event_types.insert(content::TTS_EVENT_WORD);
  required_event_types.insert(content::TTS_EVENT_END);
  in_utterance->SetRequiredEventTypes(required_event_types);

  std::set<content::TtsEventType> desired_event_types;
  desired_event_types.insert(content::TTS_EVENT_CANCELLED);
  desired_event_types.insert(content::TTS_EVENT_RESUME);
  desired_event_types.insert(content::TTS_EVENT_PAUSE);
  in_utterance->SetDesiredEventTypes(desired_event_types);

  // Round trip conversion to and from mojom utterance.
  auto mojo_utterance = tts_crosapi_util::ToMojo(in_utterance.get());
  mojo_utterance->browser_context_id = kBrowerContextId;

  {
    // Create TtsUtterance for a Lacros Utterance.
    std::unique_ptr<content::TtsUtterance> out_utterance =
        tts_crosapi_util::CreateUtteranceFromMojo(
            mojo_utterance, /*should_always_be_spoken=*/true);

    ASSERT_EQ(out_utterance->GetText(), kText);
    ASSERT_EQ(out_utterance->GetVoiceName(), kVoiceName);
    ASSERT_EQ(out_utterance->GetLang(), kLang);
    ASSERT_EQ(out_utterance->GetSrcId(), kSrcId);
    ASSERT_EQ(out_utterance->GetSrcUrl(), GURL(kSrcUrl));
    ASSERT_EQ(out_utterance->GetEngineId(), kEngineId);
    auto continuouse_params = out_utterance->GetContinuousParameters();
    ASSERT_EQ(continuouse_params.rate, kRate);
    ASSERT_EQ(continuouse_params.pitch, kPitch);
    ASSERT_EQ(continuouse_params.volume, kVolume);
    ASSERT_TRUE(out_utterance->GetShouldClearQueue());

    ASSERT_TRUE(EventTypesMatches(in_utterance->GetRequiredEventTypes(),
                                  out_utterance->GetRequiredEventTypes()));
    ASSERT_TRUE(EventTypesMatches(in_utterance->GetDesiredEventTypes(),
                                  out_utterance->GetDesiredEventTypes()));

    ASSERT_TRUE(out_utterance->ShouldAlwaysBeSpoken());
  }

  {
    // Create TtsUtterance for an Ash Utterance.
    std::unique_ptr<content::TtsUtterance> out_utterance =
        tts_crosapi_util::CreateUtteranceFromMojo(
            mojo_utterance, /*should_always_be_spoken=*/false);

    ASSERT_EQ(out_utterance->GetText(), kText);
    ASSERT_EQ(out_utterance->GetVoiceName(), kVoiceName);
    ASSERT_EQ(out_utterance->GetLang(), kLang);
    ASSERT_EQ(out_utterance->GetSrcId(), kSrcId);
    ASSERT_EQ(out_utterance->GetSrcUrl(), GURL(kSrcUrl));
    ASSERT_EQ(out_utterance->GetEngineId(), kEngineId);
    auto continuouse_params = out_utterance->GetContinuousParameters();
    ASSERT_EQ(continuouse_params.rate, kRate);
    ASSERT_EQ(continuouse_params.pitch, kPitch);
    ASSERT_EQ(continuouse_params.volume, kVolume);
    ASSERT_TRUE(out_utterance->GetShouldClearQueue());

    ASSERT_TRUE(EventTypesMatches(in_utterance->GetRequiredEventTypes(),
                                  out_utterance->GetRequiredEventTypes()));
    ASSERT_TRUE(EventTypesMatches(in_utterance->GetDesiredEventTypes(),
                                  out_utterance->GetDesiredEventTypes()));

    ASSERT_FALSE(out_utterance->ShouldAlwaysBeSpoken());
  }
}

TEST_F(TtsUtteranceMojomTest, RoundTripWithOptions) {
  std::unique_ptr<content::TtsUtterance> in_utterance =
      content::TtsUtterance::Create();
  base::Value::Dict in_options;
  in_options.Set(tts_extension_api_constants::kVoiceNameKey, kVoiceName);
  in_options.Set(tts_extension_api_constants::kRateKey, kRate);
  in_options.Set(tts_extension_api_constants::kEnqueueKey, true);
  in_utterance->SetOptions(std::move(in_options));

  auto mojo_utterance = tts_crosapi_util::ToMojo(in_utterance.get());
  std::unique_ptr<content::TtsUtterance> out_utterance =
      tts_crosapi_util::CreateUtteranceFromMojo(
          mojo_utterance, /*should_always_be_spoken=*/true);

  auto* out_options = out_utterance->GetOptions();

  const base::Value* voice_value =
      out_options->Find(tts_extension_api_constants::kVoiceNameKey);
  ASSERT_TRUE(voice_value);
  ASSERT_TRUE(voice_value->is_string());
  ASSERT_EQ(voice_value->GetString(), kVoiceName);

  const base::Value* rate_value =
      out_options->Find(tts_extension_api_constants::kRateKey);
  ASSERT_TRUE(rate_value);
  ASSERT_TRUE(rate_value->is_double());
  ASSERT_EQ(rate_value->GetDouble(), kRate);

  const base::Value* enqueue_value =
      out_options->Find(tts_extension_api_constants::kEnqueueKey);
  ASSERT_TRUE(enqueue_value);
  ASSERT_TRUE(enqueue_value->is_bool());
  ASSERT_TRUE(enqueue_value->GetBool());
}

TEST_F(TtsUtteranceMojomTest, WasCreatedWithNoWebContents) {
  std::unique_ptr<content::TtsUtterance> in_utterance =
      content::TtsUtterance::Create();
  ASSERT_FALSE(in_utterance->GetWebContents());
  auto mojo_utterance = tts_crosapi_util::ToMojo(in_utterance.get());
  ASSERT_FALSE(mojo_utterance->was_created_with_web_contents);
}

TEST_F(TtsUtteranceMojomTest, WasCreatedWithWebContents) {
  content::WebContents* web_contents = GetWebContents();
  std::unique_ptr<content::TtsUtterance> in_utterance =
      content::TtsUtterance::Create(web_contents);
  auto mojo_utterance = tts_crosapi_util::ToMojo(in_utterance.get());
  ASSERT_TRUE(mojo_utterance->was_created_with_web_contents);

  // Finish |in_utterance| so that it can be destructed without DCHECK
  // failure.
  in_utterance->Finish();
}