// Copyright 2023 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/accessibility/service/speech_recognition_impl.h"
#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/extensions/speech/speech_recognition_private_recognizer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/speech/speech_recognition_test_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "media/mojo/mojom/speech_recognizer.mojom.h"
#include "services/accessibility/public/mojom/assistive_technology_type.mojom.h"
#include "services/accessibility/public/mojom/speech_recognition.mojom.h"
namespace {
// A mock implementation of the SpeechRecognitionEventObserver interface. When
// speech recognition starts, we can bind the resulting pending receiver to
// this implementation, allowing us to know when speech recognition events are
// dispatched.
class MockSpeechRecognitionEventObserverImpl
: public ax::mojom::SpeechRecognitionEventObserver {
public:
MockSpeechRecognitionEventObserverImpl(
mojo::PendingReceiver<ax::mojom::SpeechRecognitionEventObserver>
pending_receiver,
base::RepeatingCallback<void()> on_stop_callback,
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionResultEventPtr)>
on_result_callback,
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionErrorEventPtr)>
on_error_callback)
: receiver_(this, std::move(pending_receiver)),
on_stop_callback_(std::move(on_stop_callback)),
on_result_callback_(std::move(on_result_callback)),
on_error_callback_(std::move(on_error_callback)) {}
MockSpeechRecognitionEventObserverImpl(
const MockSpeechRecognitionEventObserverImpl&) = delete;
MockSpeechRecognitionEventObserverImpl& operator=(
const MockSpeechRecognitionEventObserverImpl&) = delete;
~MockSpeechRecognitionEventObserverImpl() override {}
void OnStop() override { on_stop_callback_.Run(); }
void OnResult(ax::mojom::SpeechRecognitionResultEventPtr event) override {
on_result_callback_.Run(std::move(event));
}
void OnError(ax::mojom::SpeechRecognitionErrorEventPtr event) override {
on_error_callback_.Run(std::move(event));
}
private:
mojo::Receiver<ax::mojom::SpeechRecognitionEventObserver> receiver_;
base::RepeatingCallback<void()> on_stop_callback_;
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionResultEventPtr)>
on_result_callback_;
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionErrorEventPtr)>
on_error_callback_;
};
} // namespace
namespace ash {
// Tests for SpeechRecognitionImpl - this file mostly tests that the class
// correctly manages its internal state and can dispatch events to the proper
// event observers.
class SpeechRecognitionImplTest : public InProcessBrowserTest {
protected:
void SetUpOnMainThread() override {
sr_test_helper_ = std::make_unique<SpeechRecognitionTestHelper>(
speech::SpeechRecognitionType::kNetwork,
media::mojom::RecognizerClientType::kDictation);
sr_test_helper_->SetUp(browser()->profile());
sr_impl_ = std::make_unique<SpeechRecognitionImpl>(browser()->profile());
}
void TearDownOnMainThread() override { sr_impl_.reset(); }
void HandleSpeechRecognitionStopped(const std::string& key) {
sr_impl_->HandleSpeechRecognitionStopped(key);
}
void HandleSpeechRecognitionResult(const std::string& key,
const std::u16string& transcript,
bool is_final) {
sr_impl_->HandleSpeechRecognitionResult(key, transcript, is_final);
}
void HandleSpeechRecognitionError(const std::string& key,
const std::string& error) {
sr_impl_->HandleSpeechRecognitionError(key, error);
}
extensions::SpeechRecognitionPrivateRecognizer* GetSpeechRecognizer(
const std::string& key) {
return sr_impl_->GetSpeechRecognizer(key);
}
void CreateEventObserverWrapper(const std::string& key) {
sr_impl_->CreateEventObserverWrapper(key);
}
SpeechRecognitionImpl::SpeechRecognitionEventObserverWrapper*
GetEventObserverWrapper(const std::string& key) {
return sr_impl_->GetEventObserverWrapper(key);
}
std::string CreateKey(ax::mojom::AssistiveTechnologyType type) {
return sr_impl_->CreateKey(type);
}
int GetNumRecognizers() { return sr_impl_->recognizers_.size(); }
int GetNumEventObserverWrappers() {
return sr_impl_->event_observer_wrappers_.size();
}
std::unique_ptr<SpeechRecognitionTestHelper> sr_test_helper_;
std::unique_ptr<SpeechRecognitionImpl> sr_impl_;
};
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, GetSpeechRecognizer) {
extensions::SpeechRecognitionPrivateRecognizer* first = nullptr;
extensions::SpeechRecognitionPrivateRecognizer* second = nullptr;
first = GetSpeechRecognizer("Hello");
second = GetSpeechRecognizer("Hello");
ASSERT_NE(nullptr, first);
ASSERT_NE(nullptr, second);
ASSERT_EQ(first, second);
second = GetSpeechRecognizer("World");
ASSERT_NE(nullptr, second);
ASSERT_NE(first, second);
}
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, GetEventObserverWrapper) {
SpeechRecognitionImpl::SpeechRecognitionEventObserverWrapper* first = nullptr;
SpeechRecognitionImpl::SpeechRecognitionEventObserverWrapper* second =
nullptr;
ASSERT_EQ(nullptr, GetEventObserverWrapper("Hello"));
CreateEventObserverWrapper("Hello");
first = GetEventObserverWrapper("Hello");
second = GetEventObserverWrapper("Hello");
ASSERT_NE(nullptr, first);
ASSERT_NE(nullptr, second);
ASSERT_EQ(first, second);
CreateEventObserverWrapper("World");
second = GetEventObserverWrapper("World");
ASSERT_NE(nullptr, second);
ASSERT_NE(first, second);
}
// Verifies that speech recognition can be started and stopped, and that the
// correct number of recognizers and observers are created.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, StartAndStop) {
ASSERT_EQ(0, GetNumRecognizers());
ASSERT_EQ(0, GetNumEventObserverWrappers());
auto start_options = ax::mojom::StartOptions::New();
start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Start(std::move(start_options), base::DoNothing());
sr_test_helper_->WaitForRecognitionStarted();
ASSERT_EQ(1, GetNumRecognizers());
ASSERT_EQ(1, GetNumEventObserverWrappers());
auto stop_options = ax::mojom::StopOptions::New();
stop_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Stop(std::move(stop_options), base::DoNothing());
sr_test_helper_->WaitForRecognitionStopped();
// Note that recognizers are kept alive because they can be re-used when
// starting a new session of speech recognition. Event observer wrappers
// are only valid during a session of speech recognition and should be
// recreated when a new session starts.
ASSERT_EQ(1, GetNumRecognizers());
ASSERT_EQ(0, GetNumEventObserverWrappers());
}
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, StartAndStopWithClientId) {
auto start_options = ax::mojom::StartOptions::New();
start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Start(std::move(start_options), base::DoNothing());
sr_test_helper_->WaitForRecognitionStarted();
ASSERT_EQ(1, GetNumRecognizers());
ASSERT_EQ(1, GetNumEventObserverWrappers());
auto stop_options = ax::mojom::StopOptions::New();
stop_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Stop(std::move(stop_options), base::DoNothing());
sr_test_helper_->WaitForRecognitionStopped();
}
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, StartOptions) {
base::RunLoop waiter;
auto options = ax::mojom::StartOptions::New();
ax::mojom::AssistiveTechnologyType type =
ax::mojom::AssistiveTechnologyType::kDictation;
std::string key = CreateKey(type);
options->type = type;
options->locale = "ja-JP";
options->interim_results = true;
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionStartInfoPtr info)>
callback = base::BindLambdaForTesting(
[&waiter, &key, this](ax::mojom::SpeechRecognitionStartInfoPtr info) {
auto* recognizer = GetSpeechRecognizer(key);
ASSERT_EQ("ja-JP", recognizer->locale());
ASSERT_EQ(true, recognizer->interim_results());
ASSERT_TRUE(info->observer_or_error->is_observer());
ASSERT_FALSE(info->observer_or_error->is_error());
waiter.Quit();
});
sr_impl_->Start(std::move(options), std::move(callback));
waiter.Run();
}
// Verifies that SpeechRecognitionImpl notifies the correct event observer.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, DispatchStopEvent) {
base::RunLoop waiter;
// Called when a speech recognition stop event comes through.
base::RepeatingCallback<void()> on_stop_callback =
base::BindLambdaForTesting([&waiter]() { waiter.Quit(); });
std::unique_ptr<MockSpeechRecognitionEventObserverImpl>
mock_event_observer_impl;
auto start_options = ax::mojom::StartOptions::New();
ax::mojom::AssistiveTechnologyType type =
ax::mojom::AssistiveTechnologyType::kDictation;
std::string key = CreateKey(type);
start_options->type = type;
sr_impl_->Start(
std::move(start_options),
base::BindLambdaForTesting(
[&mock_event_observer_impl, &on_stop_callback, &key,
this](ax::mojom::SpeechRecognitionStartInfoPtr info) {
ASSERT_EQ(1, GetNumEventObserverWrappers());
mock_event_observer_impl =
std::make_unique<MockSpeechRecognitionEventObserverImpl>(
std::move(info->observer_or_error->get_observer()),
std::move(on_stop_callback), base::DoNothing(),
base::DoNothing());
// Calling this method will dispatch a request to
// `mock_event_observer_impl`.
HandleSpeechRecognitionStopped(key);
}));
sr_test_helper_->WaitForRecognitionStarted();
waiter.Run();
ASSERT_EQ(0, GetNumEventObserverWrappers());
}
// Verifies that speech recognition results can be returned.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, DispatchResultEvent) {
// Variables used throughout the test.
base::RunLoop waiter;
ax::mojom::AssistiveTechnologyType type =
ax::mojom::AssistiveTechnologyType::kDictation;
std::string key = CreateKey(type);
// Called after speech recognition has been stopped.
base::RepeatingCallback<void()> on_stop_callback =
base::BindLambdaForTesting([&waiter]() { waiter.Quit(); });
// Called when a speech recognition result has been returned.
base::RepeatingCallback<void(
ax::mojom::SpeechRecognitionResultEventPtr event)>
on_result_callback = base::BindLambdaForTesting(
[this, &key](ax::mojom::SpeechRecognitionResultEventPtr event) {
ASSERT_EQ("Hello world", event->transcript);
ASSERT_EQ(true, event->is_final);
HandleSpeechRecognitionStopped(key);
});
std::unique_ptr<MockSpeechRecognitionEventObserverImpl>
mock_event_observer_impl;
auto start_options = ax::mojom::StartOptions::New();
start_options->type = type;
sr_impl_->Start(
std::move(start_options),
base::BindLambdaForTesting(
[&mock_event_observer_impl, &on_stop_callback, &on_result_callback,
&key, this](ax::mojom::SpeechRecognitionStartInfoPtr info) {
mock_event_observer_impl =
std::make_unique<MockSpeechRecognitionEventObserverImpl>(
std::move(info->observer_or_error->get_observer()),
std::move(on_stop_callback), std::move(on_result_callback),
base::DoNothing());
// Calling this method will dispatch a request to
// `mock_event_observer_impl`.
HandleSpeechRecognitionResult(
/*key=*/key, /*transcript=*/u"Hello world", /*is_final=*/true);
}));
waiter.Run();
}
// Verifies that SpeechRecognitionImpl notifies the correct event observer
// when dispatching error events.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, DispatchErrorEvent) {
// Variables used throughout the test.
base::RunLoop waiter;
ax::mojom::AssistiveTechnologyType type =
ax::mojom::AssistiveTechnologyType::kDictation;
std::string key = CreateKey(type);
// Called when a speech recognition result has been returned.
base::RepeatingCallback<void(ax::mojom::SpeechRecognitionErrorEventPtr event)>
on_error_callback = base::BindLambdaForTesting(
[&waiter](ax::mojom::SpeechRecognitionErrorEventPtr event) {
ASSERT_EQ("Hello world", event->message);
waiter.Quit();
});
std::unique_ptr<MockSpeechRecognitionEventObserverImpl>
mock_event_observer_impl;
auto start_options = ax::mojom::StartOptions::New();
start_options->type = type;
sr_impl_->Start(
std::move(start_options),
base::BindLambdaForTesting(
[&mock_event_observer_impl, &on_error_callback, &key,
this](ax::mojom::SpeechRecognitionStartInfoPtr info) {
ASSERT_EQ(1, GetNumEventObserverWrappers());
mock_event_observer_impl =
std::make_unique<MockSpeechRecognitionEventObserverImpl>(
std::move(info->observer_or_error->get_observer()),
base::DoNothing(), base::DoNothing(),
std::move(on_error_callback));
// Calling this method will dispatch a request to
// `mock_event_observer_impl`.
HandleSpeechRecognitionError(
/*key=*/key, /*error=*/"Hello world");
}));
waiter.Run();
ASSERT_EQ(0, GetNumEventObserverWrappers());
}
// Triggers an error by attempting to start speech recognition twice and
// verifies that the correct error message is returned.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, StartError) {
auto first_start_options = ax::mojom::StartOptions::New();
first_start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Start(std::move(first_start_options), base::DoNothing());
sr_test_helper_->WaitForRecognitionStarted();
base::RunLoop waiter;
auto second_start_options = ax::mojom::StartOptions::New();
second_start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
sr_impl_->Start(std::move(second_start_options),
base::BindLambdaForTesting(
[&waiter](ax::mojom::SpeechRecognitionStartInfoPtr info) {
ASSERT_EQ("Speech recognition already started",
info->observer_or_error->get_error());
ASSERT_FALSE(info->observer_or_error->is_observer());
waiter.Quit();
}));
waiter.Run();
}
// Triggers an error by attempting to stop speech recognition when it is already
// stopped and verifies that the correct error message is returned.
IN_PROC_BROWSER_TEST_F(SpeechRecognitionImplTest, StopError) {
auto stop_options = ax::mojom::StopOptions::New();
stop_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
base::RunLoop waiter;
sr_impl_->Stop(std::move(stop_options),
base::BindLambdaForTesting(
[&waiter](const std::optional<std::string>& error) {
ASSERT_EQ("Speech recognition already stopped",
error.value());
waiter.Quit();
}));
waiter.Run();
}
} // namespace ash