// 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/test/chromeos/standalone_browser_test_controller.h"
#include <memory>
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/test/values_test_util.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/speech/tts_crosapi_util.h"
#include "chrome/browser/ui/webui/print_preview/extension_printer_service_provider_lacros.h"
#include "chrome/browser/ui/webui/print_preview/printer_handler.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chromeos/crosapi/mojom/tts.mojom-forward.h"
#include "components/prefs/pref_service.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "content/public/browser/tts_utterance.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "url/origin.h"
namespace {
blink::mojom::DisplayMode WindowModeToDisplayMode(
apps::WindowMode window_mode) {
switch (window_mode) {
case apps::WindowMode::kBrowser:
return blink::mojom::DisplayMode::kBrowser;
case apps::WindowMode::kTabbedWindow:
return blink::mojom::DisplayMode::kTabbed;
case apps::WindowMode::kWindow:
return blink::mojom::DisplayMode::kStandalone;
case apps::WindowMode::kUnknown:
return blink::mojom::DisplayMode::kUndefined;
}
}
web_app::mojom::UserDisplayMode WindowModeToUserDisplayMode(
apps::WindowMode window_mode) {
switch (window_mode) {
case apps::WindowMode::kBrowser:
return web_app::mojom::UserDisplayMode::kBrowser;
case apps::WindowMode::kTabbedWindow:
return web_app::mojom::UserDisplayMode::kTabbed;
case apps::WindowMode::kWindow:
return web_app::mojom::UserDisplayMode::kStandalone;
case apps::WindowMode::kUnknown:
return web_app::mojom::UserDisplayMode::kBrowser;
}
}
void IsolatedWebAppInstallationDone(
const webapps::AppId& installed_app_id,
StandaloneBrowserTestController::InstallIsolatedWebAppCallback callback,
base::expected<web_app::InstallIsolatedWebAppCommandSuccess,
web_app::InstallIsolatedWebAppCommandError> install_result) {
if (install_result.has_value()) {
std::move(callback).Run(
crosapi::mojom::InstallWebAppResult::NewAppId(installed_app_id));
} else {
std::move(callback).Run(
crosapi::mojom::InstallWebAppResult::NewErrorMessage(
install_result.error().message));
}
}
void OnIsolatedWebAppUrlInfoCreated(
const web_app::IsolatedWebAppInstallSource& install_source,
StandaloneBrowserTestController::InstallIsolatedWebAppCallback callback,
base::expected<web_app::IsolatedWebAppUrlInfo, std::string>
iwa_url_info_expected) {
ASSIGN_OR_RETURN(
auto iwa_url_info, iwa_url_info_expected, [&](const std::string& error) {
std::move(callback).Run(
crosapi::mojom::InstallWebAppResult::NewErrorMessage(error));
});
if (!install_source.source().dev_mode()) {
web_app::AddTrustedWebBundleIdForTesting(iwa_url_info.web_bundle_id());
}
Profile* profile = ProfileManager::GetPrimaryUserProfile();
auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
provider->scheduler().InstallIsolatedWebApp(
iwa_url_info, install_source, /*expected_version=*/std::nullopt,
/*optional_keep_alive=*/nullptr, /*optional_profile_keep_alive=*/nullptr,
base::BindOnce(&IsolatedWebAppInstallationDone, iwa_url_info.app_id(),
std::move(callback)));
}
class FakeExtensionPrinterHandler : public printing::PrinterHandler {
public:
FakeExtensionPrinterHandler() = default;
FakeExtensionPrinterHandler(const FakeExtensionPrinterHandler&) = delete;
FakeExtensionPrinterHandler& operator=(const FakeExtensionPrinterHandler&) =
delete;
~FakeExtensionPrinterHandler() override = default;
void Reset() override {}
void StartGetPrinters(AddedPrintersCallback added_printers_callback,
GetPrintersDoneCallback done_callback) override {
added_printers_callback.Run(base::test::ParseJsonList(R"(
[ {
"description": "A virtual printer for testing",
"extensionId": "jbljdigmdjodgkcllikhggoepmmffbam",
"extensionName": "Test Printer Provider",
"id": "jbljdigmdjodgkcllikhggoepmmffbam:test-printer-01",
"name": "Test Printer 01"
}, {
"description": "A virtual printer for testing",
"extensionId": "jbljdigmdjodgkcllikhggoepmmffbam",
"extensionName": "Test Printer Provider",
"id": "jbljdigmdjodgkcllikhggoepmmffbam:test-printer-02",
"name": "Test Printer 02"
} ]
)"));
std::move(done_callback).Run();
}
void StartGetCapability(const std::string& destination_id,
GetCapabilityCallback callback) override {
std::move(callback).Run(base::test::ParseJsonDict(R"(
{
"version": "1.0",
"printer": {
"supported_content_type": [
{"content_type": "application/pdf"}
]
}
})"));
}
void StartGrantPrinterAccess(const std::string& printer_id,
GetPrinterInfoCallback callback) override {
base::Value::Dict printer_info;
printer_info.Set("printerId", printer_id);
printer_info.Set("name", "Test Printer 01");
std::move(callback).Run(std::move(printer_info));
}
void StartPrint(const std::u16string& job_title,
base::Value::Dict settings,
scoped_refptr<base::RefCountedMemory> print_data,
PrintCallback callback) override {
// Simulate a successful print job. "OK" means successful.
std::move(callback).Run(base::Value("OK"));
}
};
} // namespace
// With Lacros tts support enabled, all Lacros utterances will be sent to
// Ash to be processed by TtsController in Ash. When the utterance is spoken
// by a speech engine (provided by Ash or Lacros), we need to make sure that
// Tts events are routed back to the UtteranceEventDelegate in Lacros.
// This class can be set as UtteranceEventDelegate for Lacros Utterance used
// for testing.
class StandaloneBrowserTestController::LacrosUtteranceEventDelegate
: public content::UtteranceEventDelegate {
public:
LacrosUtteranceEventDelegate(
StandaloneBrowserTestController* controller,
mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> client)
: controller_(controller), client_(std::move(client)) {}
LacrosUtteranceEventDelegate(const LacrosUtteranceEventDelegate&) = delete;
LacrosUtteranceEventDelegate& operator=(const LacrosUtteranceEventDelegate&) =
delete;
~LacrosUtteranceEventDelegate() override = default;
// content::UtteranceEventDelegate methods:
void OnTtsEvent(content::TtsUtterance* utterance,
content::TtsEventType event_type,
int char_index,
int char_length,
const std::string& error_message) override {
// Forward the TtsEvent back to ash, so that ash browser test can verify
// that TtsEvent has been routed to the UtteranceEventDelegate in Lacros.
client_->OnTtsEvent(tts_crosapi_util::ToMojo(event_type), char_index,
char_length, error_message);
if (utterance->IsFinished()) {
controller_->OnUtteranceFinished(utterance->GetId());
}
// Note: |this| is deleted if utterance->IsFinished().
}
private:
// |controller_| is guaranteed to be valid during the lifetime of this class.
const raw_ptr<StandaloneBrowserTestController> controller_;
mojo::Remote<crosapi::mojom::TtsUtteranceClient> client_;
};
StandaloneBrowserTestController::StandaloneBrowserTestController(
mojo::Remote<crosapi::mojom::TestController>& test_controller) {
CHECK_IS_TEST();
test_controller->RegisterStandaloneBrowserTestController(
controller_receiver_.BindNewPipeAndPassRemoteWithVersion());
test_controller.FlushAsync();
}
StandaloneBrowserTestController::~StandaloneBrowserTestController() = default;
void StandaloneBrowserTestController::InstallWebApp(
const std::string& start_url,
apps::WindowMode window_mode,
InstallWebAppCallback callback) {
auto info =
web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GURL(start_url));
info->title = u"Test Web App";
info->display_mode = WindowModeToDisplayMode(window_mode);
info->user_display_mode = WindowModeToUserDisplayMode(window_mode);
Profile* profile = ProfileManager::GetPrimaryUserProfile();
auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
provider->scheduler().InstallFromInfoNoIntegrationForTesting(
std::move(info),
/*overwrite_existing_manifest_fields=*/false,
webapps::WebappInstallSource::SYNC,
base::BindOnce(&StandaloneBrowserTestController::WebAppInstallationDone,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void StandaloneBrowserTestController::InstallUnpackedExtension(
const std::string& path,
InstallUnpackedExtensionCallback callback) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
extensions::ChromeTestExtensionLoader loader(profile);
loader.LoadUnpackedExtensionAsync(
base::FilePath{path},
base::BindOnce([](const extensions::Extension* extension) {
return extension->id();
}).Then(std::move(callback)));
}
void StandaloneBrowserTestController::ObserveDomMessages(
mojo::PendingRemote<crosapi::mojom::DomMessageObserver> observer,
ObserveDomMessagesCallback callback) {
dom_message_observer_.Bind(std::move(observer));
dom_message_observer_.set_disconnect_handler(base::BindOnce(
&StandaloneBrowserTestController::OnDomMessageObserverDisconnected,
weak_ptr_factory_.GetWeakPtr()));
ASSERT_FALSE(dom_message_queue_.has_value());
dom_message_queue_.emplace();
dom_message_queue_->SetOnMessageAvailableCallback(
base::BindOnce(&StandaloneBrowserTestController::OnDomMessageQueueReady,
weak_ptr_factory_.GetWeakPtr()));
std::move(callback).Run();
}
void StandaloneBrowserTestController::OnDomMessageObserverDisconnected() {
dom_message_queue_.reset();
dom_message_observer_.reset();
}
void StandaloneBrowserTestController::OnDomMessageQueueReady() {
std::string message;
ASSERT_TRUE(dom_message_queue_->PopMessage(&message));
dom_message_observer_->OnMessage(message);
dom_message_queue_->SetOnMessageAvailableCallback(
base::BindOnce(&StandaloneBrowserTestController::OnDomMessageQueueReady,
weak_ptr_factory_.GetWeakPtr()));
}
void StandaloneBrowserTestController::InstallComponentExtension(
const std::string& path,
const std::string& extension_id,
InstallComponentExtensionCallback callback) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile)->extension_service();
service->component_loader()->AddComponentFromDirWithManifestFilename(
base::FilePath(path), extension_id, extensions::kManifestFilename,
extensions::kManifestFilename, std::move(callback));
}
void StandaloneBrowserTestController::RemoveComponentExtension(
const std::string& extension_id,
RemoveComponentExtensionCallback callback) {
Profile* profile = ProfileManager::GetPrimaryUserProfile();
extensions::ExtensionSystem::Get(profile)
->extension_service()
->RemoveComponentExtension(extension_id);
std::move(callback).Run();
}
void StandaloneBrowserTestController::LoadVpnExtension(
const std::string& extension_name,
LoadVpnExtensionCallback callback) {
std::string error;
auto extension = extensions::Extension::Create(
base::FilePath(), extensions::mojom::ManifestLocation::kUnpacked,
CreateVpnExtensionManifest(extension_name),
extensions::Extension::NO_FLAGS, &error);
if (!error.empty()) {
std::move(callback).Run(error);
return;
}
auto* extension_registry = extensions::ExtensionRegistry::Get(
ProfileManager::GetPrimaryUserProfile());
extension_registry->AddEnabled(extension);
extension_registry->TriggerOnLoaded(extension.get());
std::move(callback).Run(extension->id());
}
void StandaloneBrowserTestController::GetTtsVoices(
GetTtsVoicesCallback callback) {
std::vector<content::VoiceData> voices;
tts_crosapi_util::GetAllVoicesForTesting( // IN-TEST
ProfileManager::GetActiveUserProfile(), GURL(), &voices);
std::vector<crosapi::mojom::TtsVoicePtr> mojo_voices;
for (const auto& voice : voices) {
mojo_voices.push_back(tts_crosapi_util::ToMojo(voice));
}
std::move(callback).Run(std::move(mojo_voices));
}
void StandaloneBrowserTestController::TtsSpeak(
crosapi::mojom::TtsUtterancePtr mojo_utterance,
mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> utterance_client) {
std::unique_ptr<content::TtsUtterance> lacros_utterance =
tts_crosapi_util::CreateUtteranceFromMojo(
mojo_utterance, /*should_always_be_spoken=*/true);
auto event_delegate = std::make_unique<LacrosUtteranceEventDelegate>(
this, std::move(utterance_client));
lacros_utterance->SetEventDelegate(event_delegate.get());
lacros_utterance_event_delegates_.emplace(lacros_utterance->GetId(),
std ::move(event_delegate));
tts_crosapi_util::SpeakForTesting(std::move(lacros_utterance));
}
void StandaloneBrowserTestController::InstallSubApp(
const webapps::AppId& parent_app_id,
const std::string& sub_app_path,
InstallSubAppCallback callback) {
GURL parent_app_url(
base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator,
parent_app_id}));
GURL start_url = parent_app_url.Resolve(sub_app_path);
auto info =
web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
info->parent_app_id = parent_app_id;
info->parent_app_manifest_id = parent_app_url;
info->title = u"Sub App";
Profile* profile = ProfileManager::GetPrimaryUserProfile();
auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
provider->scheduler().InstallFromInfoNoIntegrationForTesting(
std::move(info),
/*overwrite_existing_manifest_fields=*/false,
webapps::WebappInstallSource::SUB_APP,
base::BindOnce(&StandaloneBrowserTestController::WebAppInstallationDone,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void StandaloneBrowserTestController::InstallIsolatedWebApp(
crosapi::mojom::IsolatedWebAppLocationPtr location,
bool dev_mode,
InstallIsolatedWebAppCallback callback) {
web_app::IsolatedWebAppInstallSource install_source = ([&]() {
if (dev_mode) {
if (location->is_bundle_path()) {
return web_app::IsolatedWebAppInstallSource::FromDevUi(
web_app::IwaSourceBundleDevModeWithFileOp(
base::FilePath(location->get_bundle_path()),
web_app::IwaSourceBundleDevFileOp::kCopy));
} else {
return web_app::IsolatedWebAppInstallSource::FromDevUi(
web_app::IwaSourceProxy(
url::Origin::Create(location->get_proxy_origin())));
}
} else {
return web_app::IsolatedWebAppInstallSource::FromGraphicalInstaller(
web_app::IwaSourceBundleProdModeWithFileOp(
base::FilePath(location->get_bundle_path()),
web_app::IwaSourceBundleProdFileOp::kCopy));
}
})();
web_app::IsolatedWebAppUrlInfo::CreateFromIsolatedWebAppSource(
install_source.source(),
base::BindOnce(&OnIsolatedWebAppUrlInfoCreated, install_source,
std::move(callback)));
}
void StandaloneBrowserTestController::SetWebAppSettingsPref(
const std::string& policy,
SetWebAppSettingsPrefCallback callback) {
CHECK(callback);
auto result = base::JSONReader::ReadAndReturnValueWithError(
policy, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
if (!result.has_value()) {
std::move(callback).Run(/*success=*/false);
return;
}
if (!result->is_list()) {
std::move(callback).Run(/*success=*/false);
return;
}
ProfileManager::GetPrimaryUserProfile()->GetPrefs()->SetList(
prefs::kWebAppSettings, std::move(*result).TakeList());
std::move(callback).Run(/*success=*/true);
}
void StandaloneBrowserTestController::SetWebAppInstallForceListPref(
const std::string& policy,
SetWebAppInstallForceListPrefCallback callback) {
CHECK(callback);
auto result = base::JSONReader::ReadAndReturnValueWithError(
policy, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
if (!result.has_value()) {
std::move(callback).Run(/*success=*/false);
return;
}
if (!result->is_list()) {
std::move(callback).Run(/*success=*/false);
return;
}
ProfileManager::GetPrimaryUserProfile()->GetPrefs()->SetList(
prefs::kWebAppInstallForceList, std::move(*result).TakeList());
std::move(callback).Run(/*success=*/true);
}
// Uses a fake extension printer handler to process printing requests from ash
// browser tests.
void StandaloneBrowserTestController::SetFakeExtensionPrinterHandler(
SetFakeExtensionPrinterHandlerCallback callback) {
printing::ExtensionPrinterServiceProviderLacros::GetForBrowserContext(
ProfileManager::GetPrimaryUserProfile())
->SetPrinterHandlerForTesting(
std::make_unique<FakeExtensionPrinterHandler>());
std::move(callback).Run();
}
void StandaloneBrowserTestController::OnUtteranceFinished(int utterance_id) {
// Delete the utterace event delegate object when the utterance is finished.
lacros_utterance_event_delegates_.erase(utterance_id);
}
void StandaloneBrowserTestController::GetExtensionKeeplist(
GetExtensionKeeplistCallback callback) {
auto mojo_keeplist = crosapi::mojom::ExtensionKeepList::New();
for (const auto& id :
extensions::GetExtensionsRunInOSAndStandaloneBrowser()) {
mojo_keeplist->extensions_run_in_os_and_standalonebrowser.push_back(
std::string(id));
}
for (const auto& id :
extensions::GetExtensionAppsRunInOSAndStandaloneBrowser()) {
mojo_keeplist->extension_apps_run_in_os_and_standalonebrowser.push_back(
std::string(id));
}
for (const auto& id : extensions::GetExtensionsRunInOSOnly()) {
mojo_keeplist->extensions_run_in_os_only.push_back(std::string(id));
}
for (const auto& id : extensions::GetExtensionAppsRunInOSOnly()) {
mojo_keeplist->extension_apps_run_in_os_only.push_back(std::string(id));
}
std::move(callback).Run(std::move(mojo_keeplist));
}
void StandaloneBrowserTestController::WebAppInstallationDone(
InstallWebAppCallback callback,
const webapps::AppId& installed_app_id,
webapps::InstallResultCode code) {
std::move(callback).Run(code == webapps::InstallResultCode::kSuccessNewInstall
? installed_app_id
: "");
}
base::Value::Dict StandaloneBrowserTestController::CreateVpnExtensionManifest(
const std::string& extension_name) {
base::Value::Dict manifest;
manifest.Set(extensions::manifest_keys::kName, extension_name);
manifest.Set(extensions::manifest_keys::kVersion, "1.0");
manifest.Set(extensions::manifest_keys::kManifestVersion, 2);
base::Value::List permissions;
permissions.Append("vpnProvider");
manifest.Set(extensions::manifest_keys::kPermissions, std::move(permissions));
return manifest;
}