chromium/chrome/browser/ash/extensions/speech/speech_recognition_private_manager_browsertest.cc

// Copyright 2021 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/extensions/speech/speech_recognition_private_manager.h"

#include "base/memory/raw_ptr.h"
#include "chrome/browser/ash/extensions/speech/speech_recognition_private_base_test.h"
#include "chrome/browser/ash/extensions/speech/speech_recognition_private_recognizer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/speech/speech_recognition_constants.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"

namespace {
const char kEnglishLocale[] = "en-US";
const char kExtensionId[] = "egfdjlfmgnehecnclamagfafdccgfndp";
}  // namespace

namespace extensions {

class SpeechRecognitionPrivateManagerTest
    : public SpeechRecognitionPrivateBaseTest {
 protected:
  SpeechRecognitionPrivateManagerTest() {}
  ~SpeechRecognitionPrivateManagerTest() override = default;
  SpeechRecognitionPrivateManagerTest(
      const SpeechRecognitionPrivateManagerTest&) = delete;
  SpeechRecognitionPrivateManagerTest& operator=(
      const SpeechRecognitionPrivateManagerTest&) = delete;

  void SetUpOnMainThread() override {
    SpeechRecognitionPrivateBaseTest::SetUpOnMainThread();
    manager_ = SpeechRecognitionPrivateManager::Get(profile());
  }

  void TearDownOnMainThread() override {
    manager_->recognition_data_.clear();
    SpeechRecognitionPrivateBaseTest::TearDownOnMainThread();
  }

  std::string CreateKey(const std::string& extension_id,
                        std::optional<int> client_id) {
    return manager_->CreateKey(extension_id, client_id);
  }

  void HandleStart(
      const std::string& key,
      std::optional<std::string> locale,
      std::optional<bool> interim_results,
      base::OnceCallback<void(speech::SpeechRecognitionType,
                              std::optional<std::string>)> on_start_callback) {
    manager_->HandleStart(key, locale, interim_results,
                          std::move(on_start_callback));
  }

  void HandleStopAndWait(
      const std::string& key,
      base::OnceCallback<void(std::optional<std::string>)> callback) {
    manager_->HandleStop(key, std::move(callback));
    WaitForRecognitionStopped();
  }

  SpeechRecognitionPrivateRecognizer* GetSpeechRecognizer(
      const std::string& key) {
    return manager_->GetSpeechRecognizer(key);
  }

  void DispatchOnStopEvent(const std::string& key) {
    manager_->HandleSpeechRecognitionStopped(key);
  }

  void DispatchOnResultEvent(const std::string& key,
                             const std::u16string transcript,
                             bool is_final) {
    manager_->HandleSpeechRecognitionResult(key, transcript, is_final);
  }

  void DispatchOnErrorEvent(const std::string& key,
                            const std::string& message) {
    manager_->HandleSpeechRecognitionError(key, message);
  }

 private:
  raw_ptr<SpeechRecognitionPrivateManager, DanglingUntriaged> manager_;
};

INSTANTIATE_TEST_SUITE_P(
    Network,
    SpeechRecognitionPrivateManagerTest,
    ::testing::Values(speech::SpeechRecognitionType::kNetwork));

INSTANTIATE_TEST_SUITE_P(
    OnDevice,
    SpeechRecognitionPrivateManagerTest,
    ::testing::Values(speech::SpeechRecognitionType::kOnDevice));

IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest, CreateKey) {
  ASSERT_EQ("Testing", CreateKey("Testing", std::optional<int>()));
  ASSERT_EQ("Testing.0", CreateKey("Testing", std::optional<int>(0)));
  ASSERT_EQ("Testing.1", CreateKey("Testing", std::optional<int>(1)));
}

IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest,
                       GetSpeechRecognizer) {
  SpeechRecognitionPrivateRecognizer* first_recognizer = nullptr;
  SpeechRecognitionPrivateRecognizer* second_recognizer = nullptr;
  first_recognizer = GetSpeechRecognizer("Testing");
  second_recognizer = GetSpeechRecognizer("Testing");
  ASSERT_NE(nullptr, first_recognizer);
  ASSERT_NE(nullptr, second_recognizer);
  ASSERT_EQ(first_recognizer, second_recognizer);
  second_recognizer = GetSpeechRecognizer("Testing.0");
  ASSERT_NE(nullptr, second_recognizer);
  ASSERT_NE(first_recognizer, second_recognizer);
}

IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest, HandleStart) {
  const std::string key = "Testing";
  std::optional<std::string> locale;
  std::optional<bool> interim_results(true);

  HandleStart(key, locale, interim_results, base::DoNothing());
  WaitForRecognitionStarted();
  SpeechRecognitionPrivateRecognizer* first_recognizer =
      GetSpeechRecognizer(key);
  ASSERT_NE(nullptr, first_recognizer);
  ASSERT_EQ(kEnglishLocale, first_recognizer->locale());
  ASSERT_TRUE(first_recognizer->interim_results());
  ASSERT_EQ(SPEECH_RECOGNIZER_RECOGNIZING, first_recognizer->current_state());

  // Try to change some properties and start again. Calling HandleStart() when
  // speech recognition is active should cause an error. The error message is
  // verified in SpeechRecognitionPrivateRecognizerTest. For this test, just
  // verify that properties are not updated and that speech recognition is
  // canceled.
  interim_results = false;
  HandleStart(key, locale, interim_results, base::DoNothing());
  SpeechRecognitionPrivateRecognizer* second_recognizer =
      GetSpeechRecognizer(key);
  ASSERT_NE(nullptr, second_recognizer);
  ASSERT_EQ(first_recognizer, second_recognizer);
  ASSERT_EQ(kEnglishLocale, second_recognizer->locale());
  ASSERT_TRUE(second_recognizer->interim_results());
  ASSERT_EQ(SPEECH_RECOGNIZER_OFF, second_recognizer->current_state());
}

IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest,
                       HandleStartAndStop) {
  const std::string key = "Testing";
  std::optional<std::string> locale;
  std::optional<bool> interim_results(true);

  HandleStart(key, locale, interim_results, base::DoNothing());
  WaitForRecognitionStarted();
  SpeechRecognitionPrivateRecognizer* recognizer = GetSpeechRecognizer(key);
  ASSERT_NE(nullptr, recognizer);
  ASSERT_EQ(SPEECH_RECOGNIZER_RECOGNIZING, recognizer->current_state());

  HandleStopAndWait(key, base::DoNothing());
  recognizer = GetSpeechRecognizer(key);
  ASSERT_NE(nullptr, recognizer);
  ASSERT_EQ(SPEECH_RECOGNIZER_OFF, recognizer->current_state());
}

// Tests that events can be dispatched from the SpeechRecognitionPrivateManager
// and received and processed in an extension.
IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest,
                       DispatchOnStopEvent) {
  ASSERT_TRUE(RunSpeechRecognitionPrivateTest("onstop_event")) << message_;

  const char* kExtensionIdAndIncorrectClientId =
      "egfdjlfmgnehecnclamagfafdccgfndp.0";
  const char* kCorrectExtensionIdAndClientId =
      "egfdjlfmgnehecnclamagfafdccgfndp.4";
  const char* kSkippingEvent = "Skipping event";
  const char* kProcessingEvent = "Processing event";

  // Send onStop events and ensure that we only process the event whose client
  // ID matches the extension's client ID.
  const struct {
    const char* key;
    const char* expected;
  } kTestCases[] = {{kExtensionId, kSkippingEvent},
                    {kExtensionIdAndIncorrectClientId, kSkippingEvent},
                    {kCorrectExtensionIdAndClientId, kProcessingEvent}};

  for (const auto& test : kTestCases) {
    ExtensionTestMessageListener listener(test.expected);
    DispatchOnStopEvent(test.key);
    ASSERT_TRUE(listener.WaitUntilSatisfied());
  }
}

// Tests that events can be dispatched from the SpeechRecognitionPrivateManager
// and received and processed in an extension.
IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest,
                       DispatchOnResultEvent) {
  ASSERT_TRUE(RunSpeechRecognitionPrivateTest("onresult_event")) << message_;

  const char* kFirstClient = "egfdjlfmgnehecnclamagfafdccgfndp.1";
  const char* kSecondClient = "egfdjlfmgnehecnclamagfafdccgfndp.2";
  const char* kSuccess = "Received result";
  const char* kFirstClientSkip = "Skipping event in first listener";
  const char* kSecondClientSkip = "Skipping event in second listener";
  const std::u16string kTranscript = u"This is a test";

  const struct {
    const char* key;
    const std::u16string transcript;
    const bool is_final;
    const char* expected_success_message;
    const char* expected_skip_message;
  } kTestCases[] = {
      {kFirstClient, kTranscript, false, kSuccess, kSecondClientSkip},
      {kSecondClient, kTranscript, true, kSuccess, kFirstClientSkip}};

  for (const auto& test : kTestCases) {
    // For each onResult event, verify that it was successfully handled in one
    // listener and dropped in the other (there are only two listeners).
    ExtensionTestMessageListener success_listener(
        test.expected_success_message);
    ExtensionTestMessageListener skip_listener(test.expected_skip_message);
    DispatchOnResultEvent(test.key, test.transcript, test.is_final);
    ASSERT_TRUE(success_listener.WaitUntilSatisfied());
    ASSERT_TRUE(skip_listener.WaitUntilSatisfied());
  }
}

// Tests that events can be dispatched from the SpeechRecognitionPrivateManager
// and received and processed in an extension.
IN_PROC_BROWSER_TEST_P(SpeechRecognitionPrivateManagerTest,
                       DispatchOnErrorEvent) {
  ResultCatcher result_catcher;
  ExtensionTestMessageListener listener("Proceed");

  const Extension* extension = LoadExtensionAsComponent("onerror_event");
  ASSERT_TRUE(extension);
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  DispatchOnErrorEvent(kExtensionId, "A fatal error");
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

}  // namespace extensions