// Copyright 2020 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/crosapi/test_controller_ash.h"
#include <optional>
#include <utility>
#include <vector>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/shelf_item_delegate.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_test_api.h"
#include "ash/public/cpp/split_view_test_api.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_observer.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "base/check_is_test.h"
#include "base/containers/flat_set.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/version.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/almanac_api_client/almanac_api_util.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/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/ash/crosapi/input_method_test_interface_ash.h"
#include "chrome/browser/ash/crosapi/vpn_service_ash.h"
#include "chrome/browser/ash/crosapi/window_util.h"
#include "chrome/browser/ash/printing/cups_print_job_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sharesheet/sharesheet_service.h"
#include "chrome/browser/speech/tts_crosapi_util.h"
#include "chrome/browser/ui/ash/desks/desks_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/views/tabs/tab_scrubber_chromeos.h"
#include "chrome/browser/ui/webui/ash/app_install/app_install_page_handler.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/shill/shill_profile_client.h"
#include "chromeos/ash/components/dbus/shill/shill_third_party_vpn_driver_client.h"
#include "chromeos/ash/components/dbus/userdataauth/cryptohome_misc_client.h"
#include "chromeos/ash/components/network/network_handler_test_helper.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/sync/model/string_ordinal.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/tts_utterance.h"
#include "crypto/sha2.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/bindings/type_converter.h"
#include "printing/buildflags/buildflags.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/display/manager/managed_display_info.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_source.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "url/gurl.h"
#if BUILDFLAG(USE_CUPS)
#include "chrome/browser/ash/printing/cups_print_job.h"
#include "chrome/browser/ash/printing/cups_print_job_manager_factory.h"
#include "chrome/browser/ash/printing/history/print_job_history_service.h"
#include "chrome/browser/ash/printing/history/print_job_history_service_factory.h"
#include "chrome/browser/ash/printing/history/print_job_history_service_impl.h"
#include "chrome/browser/ash/printing/history/test_print_job_database.h"
#include "chrome/browser/ash/printing/test_cups_print_job_manager.h"
#endif // BUILDFLAG(USE_CUPS)
namespace mojo {
// static
ash::SnapPosition
TypeConverter<ash::SnapPosition, crosapi::mojom::SnapPosition>::Convert(
crosapi::mojom::SnapPosition position) {
switch (position) {
case crosapi::mojom::SnapPosition::kPrimary:
return ash::SnapPosition::kPrimary;
case crosapi::mojom::SnapPosition::kSecondary:
return ash::SnapPosition::kSecondary;
}
}
} // namespace mojo
namespace crosapi {
namespace {
constexpr int kSimulatedDisplayXResolution = 640;
constexpr int kSimulatedDisplayYResolution = 480;
// Returns whether the dispatcher or target was destroyed.
bool Dispatch(aura::WindowTreeHost* host, ui::Event* event) {
ui::EventDispatchDetails dispatch_details =
host->GetEventSource()->SendEventToSink(event);
return dispatch_details.dispatcher_destroyed ||
dispatch_details.target_destroyed;
}
// Returns whether the dispatcher or target was destroyed.
bool DispatchMouseEvent(aura::Window* window,
ui::EventType type,
gfx::Point location) {
ui::MouseEvent press(type, location, location, ui::EventTimeForNow(),
ui::EF_LEFT_MOUSE_BUTTON, ui::EF_LEFT_MOUSE_BUTTON);
return Dispatch(window->GetHost(), &press);
}
// Enables or disables tablet mode and waits for the transition to finish.
void SetTabletModeEnabled(bool enabled) {
ash::TabletMode::Waiter waiter(enabled);
if (enabled) {
ash::TabletModeControllerTestApi().EnterTabletMode();
} else {
ash::TabletModeControllerTestApi().LeaveTabletMode();
}
waiter.Wait();
}
std::string GetMachineStatisticKeyString(mojom::MachineStatisticKeyType key) {
if (key == mojom::MachineStatisticKeyType::kOemDeviceRequisitionKey) {
return ash::system::kOemDeviceRequisitionKey;
}
if (key == mojom::MachineStatisticKeyType::kHardwareClassKey) {
return ash::system::kHardwareClassKey;
}
if (key == mojom::MachineStatisticKeyType::kCustomizationIdKey) {
return ash::system::kCustomizationIdKey;
}
// Return empty string for unknown key.
return "";
}
const base::TimeDelta kWindowWaitTimeout = base::Seconds(10);
TestControllerAsh* g_instance = nullptr;
} // namespace
// This class closes all the Ash browser windows and runs the callback to
// notify the callback client whether it has successfully closed all browser
// windows, or failed to do so within the timeout duration. It will destroy
// itself after running the callback.
class TestControllerAsh::SelfOwnedAshBrowserWindowCloser
: public BrowserListObserver {
public:
explicit SelfOwnedAshBrowserWindowCloser(
CloseAllAshBrowserWindowsAndConfirmCallback callback)
: callback_(std::move(callback)) {
BrowserList::AddObserver(this);
}
SelfOwnedAshBrowserWindowCloser(const SelfOwnedAshBrowserWindowCloser&) =
delete;
SelfOwnedAshBrowserWindowCloser& operator=(
const SelfOwnedAshBrowserWindowCloser&) = delete;
~SelfOwnedAshBrowserWindowCloser() override {
BrowserList::RemoveObserver(this);
}
void CloseAllBrowserWindows() {
if (BrowserList::GetInstance()->empty()) {
OnAllBrowserWindowsClosed(/*success=*/true);
// Note: |this| is deleted at this point.
return;
}
timer_.Start(
FROM_HERE, kWindowWaitTimeout,
base::BindOnce(
&SelfOwnedAshBrowserWindowCloser::OnAllBrowserWindowsClosed,
base::Unretained(this), /*success=*/false));
for (Browser* browser : *BrowserList::GetInstance()) {
// Close the browser asynchronously.
browser->window()->Close();
}
}
private:
// BrowserListObserver:
void OnBrowserRemoved(Browser* browser) override {
if (BrowserList::GetInstance()->empty()) {
OnAllBrowserWindowsClosed(/*success=*/true);
// Note: |this| is deleted at this point.
}
}
void OnAllBrowserWindowsClosed(bool success) {
std::move(callback_).Run(success);
delete this;
}
CloseAllAshBrowserWindowsAndConfirmCallback callback_;
base::OneShotTimer timer_;
};
// This class runs the callback to notify the callback client whether it has
// observed at least 1 ash browser window open, or failed to do so within the
// timeout duration. It will destroy itself after running the callback.
class TestControllerAsh::SelfOwnedAshBrowserWindowOpenWaiter
: public BrowserListObserver {
public:
explicit SelfOwnedAshBrowserWindowOpenWaiter(
CheckAtLeastOneAshBrowserWindowOpenCallback callback)
: callback_(std::move(callback)) {
BrowserList::AddObserver(this);
}
SelfOwnedAshBrowserWindowOpenWaiter(
const SelfOwnedAshBrowserWindowOpenWaiter&) = delete;
SelfOwnedAshBrowserWindowOpenWaiter& operator=(
const SelfOwnedAshBrowserWindowOpenWaiter&) = delete;
~SelfOwnedAshBrowserWindowOpenWaiter() override {
BrowserList::RemoveObserver(this);
}
void CheckIfAtLeastOneWindowOpen() {
if (BrowserList::GetInstance()->size() >= 1u) {
NotifyBrowserWindowOpen(/*has_open_window=*/true);
// Note: |this| is deleted at this point.
return;
}
timer_.Start(
FROM_HERE, kWindowWaitTimeout,
base::BindOnce(
&SelfOwnedAshBrowserWindowOpenWaiter::NotifyBrowserWindowOpen,
base::Unretained(this), /*browser_window_open=*/false));
}
private:
// BrowserListObserver:
void OnBrowserAdded(Browser* browser) override {
if (BrowserList::GetInstance()->size() >= 1u) {
NotifyBrowserWindowOpen(/*has_open_window=*/true);
// Note: |this| is deleted at this point.
}
}
// Notifies the |callback_| client whether it has observed at least 1 browser
// window open.
void NotifyBrowserWindowOpen(bool has_open_window) {
std::move(callback_).Run(has_open_window);
delete this;
}
CheckAtLeastOneAshBrowserWindowOpenCallback callback_;
base::OneShotTimer timer_;
};
TestControllerAsh* TestControllerAsh::Get() {
return g_instance;
}
TestControllerAsh::TestControllerAsh() {
CHECK_IS_TEST();
CHECK(!g_instance);
g_instance = this;
}
TestControllerAsh::~TestControllerAsh() {
CHECK_EQ(g_instance, this);
g_instance = nullptr;
}
void TestControllerAsh::BindReceiver(
mojo::PendingReceiver<mojom::TestController> receiver) {
// This interface is not available on production devices. It's only
// needed for tests that run on Linux-chrome so no reason to expose it.
#if BUILDFLAG(IS_CHROMEOS_DEVICE)
LOG(ERROR) << "Ash does not support TestController on devices";
#else
receivers_.Add(this, std::move(receiver));
#endif
}
void TestControllerAsh::ClickElement(const std::string& element_name,
ClickElementCallback callback) {
ui::ElementIdentifier id =
ui::ElementIdentifier::FromName(element_name.c_str());
if (!id) {
std::move(callback).Run(/*success=*/false);
return;
}
auto views = views::ElementTrackerViews::GetInstance()
->GetAllMatchingViewsInAnyContext(id);
if (views.empty()) {
std::move(callback).Run(/*success=*/false);
return;
}
// Pick the first view that matches the element name.
views::Button* button = views::Button::AsButton(views[0]);
if (!button) {
std::move(callback).Run(/*success=*/false);
return;
}
// We directly send mouse events to the view. It's also possible to use
// EventGenerator to move the mouse and send a click. Unfortunately, that
// approach has occasional flakiness. This is presumably due to another window
// appearing on top of the dialog and taking the mouse events but has not been
// explicitly diagnosed.
views::test::InteractionTestUtilSimulatorViews::PressButton(
button, ui::test::InteractionTestUtil::InputType::kMouse);
std::move(callback).Run(/*success=*/true);
}
void TestControllerAsh::ClickWindow(const std::string& window_id) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window)
return;
const gfx::Point center = window->bounds().CenterPoint();
bool destroyed =
DispatchMouseEvent(window, ui::EventType::kMousePressed, center);
if (!destroyed) {
DispatchMouseEvent(window, ui::EventType::kMouseReleased, center);
}
}
void TestControllerAsh::ConnectToNetwork(const std::string& service_path) {
ash::ShillServiceClient::Get()->Connect(
dbus::ObjectPath(service_path), base::DoNothing(),
ash::ShillServiceClient::ErrorCallback());
}
void TestControllerAsh::DisconnectFromNetwork(const std::string& service_path) {
ash::ShillServiceClient::Get()->Disconnect(
dbus::ObjectPath(service_path), base::DoNothing(),
ash::ShillServiceClient::ErrorCallback());
}
void TestControllerAsh::DoesItemExistInShelf(
const std::string& item_id,
DoesItemExistInShelfCallback callback) {
bool exists = ash::ShelfModel::Get()->ItemIndexByAppID(item_id) != -1;
std::move(callback).Run(exists);
}
void TestControllerAsh::DoesElementExist(const std::string& element_name,
DoesElementExistCallback callback) {
ui::ElementIdentifier id =
ui::ElementIdentifier::FromName(element_name.c_str());
if (!id) {
std::move(callback).Run(/*exists=*/false);
return;
}
bool any_elements_exist = !views::ElementTrackerViews::GetInstance()
->GetAllMatchingViewsInAnyContext(id)
.empty();
std::move(callback).Run(/*exists=*/any_elements_exist);
}
void TestControllerAsh::DoesWindowExist(const std::string& window_id,
DoesWindowExistCallback callback) {
aura::Window* window = GetShellSurfaceWindow(window_id);
// A window exists if it is either visible or minimized.
bool exists = false;
if (window) {
auto* window_state = ash::WindowState::Get(window);
exists = window->IsVisible() || window_state->IsMinimized();
}
std::move(callback).Run(exists);
}
void TestControllerAsh::EnterOverviewMode(EnterOverviewModeCallback callback) {
overview_waiters_.push_back(std::make_unique<OverviewWaiter>(
/*wait_for_enter=*/true, std::move(callback), this));
ash::Shell::Get()->overview_controller()->StartOverview(
ash::OverviewStartAction::kTests);
}
void TestControllerAsh::ExitOverviewMode(ExitOverviewModeCallback callback) {
overview_waiters_.push_back(std::make_unique<OverviewWaiter>(
/*wait_for_enter=*/false, std::move(callback), this));
ash::Shell::Get()->overview_controller()->EndOverview(
ash::OverviewEndAction::kTests);
}
void TestControllerAsh::EnterTabletMode(EnterTabletModeCallback callback) {
SetTabletModeEnabled(true);
std::move(callback).Run();
}
void TestControllerAsh::ExitTabletMode(ExitTabletModeCallback callback) {
SetTabletModeEnabled(false);
std::move(callback).Run();
}
void TestControllerAsh::GetShelfItemState(const std::string& app_id,
GetShelfItemStateCallback callback) {
ash::RootWindowController* const controller =
ash::Shell::GetRootWindowControllerWithDisplayId(
display::Screen::GetScreen()->GetPrimaryDisplay().id());
ash::ShelfView* const shelf_view =
controller->shelf()->GetShelfViewForTesting();
const ash::ShelfAppButton* const app_button =
shelf_view->GetShelfAppButton(ash::ShelfID(app_id));
uint32_t state = static_cast<uint32_t>(mojom::ShelfItemState::kNormal);
if (app_button) {
if (app_button->state() & ash::ShelfAppButton::STATE_ACTIVE)
state = static_cast<uint32_t>(mojom::ShelfItemState::kActive);
else if (app_button->state() & ash::ShelfAppButton::STATE_RUNNING)
state = static_cast<uint32_t>(mojom::ShelfItemState::kRunning);
if (app_button->state() & ash::ShelfAppButton::STATE_NOTIFICATION)
state |= static_cast<uint32_t>(mojom::ShelfItemState::kNotification);
}
std::move(callback).Run(state);
}
void TestControllerAsh::GetContextMenuForShelfItem(
const std::string& item_id,
GetContextMenuForShelfItemCallback callback) {
ash::ShelfItemDelegate* delegate =
ash::ShelfModel::Get()->GetShelfItemDelegate(ash::ShelfID(item_id));
if (!delegate) {
std::move(callback).Run({});
return;
}
delegate->GetContextMenu(
/*display_id=*/0,
base::BindOnce(&TestControllerAsh::OnGetContextMenuForShelfItem,
std::move(callback)));
}
void TestControllerAsh::GetMinimizeOnBackKeyWindowProperty(
const std::string& window_id,
GetMinimizeOnBackKeyWindowPropertyCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run(mojom::OptionalBoolean::kUnknown);
return;
}
bool* value = window->GetProperty(ash::kMinimizeOnBackKey);
if (!value) {
std::move(cb).Run(mojom::OptionalBoolean::kUnknown);
return;
}
std::move(cb).Run(*value ? mojom::OptionalBoolean::kTrue
: mojom::OptionalBoolean::kFalse);
}
void TestControllerAsh::GetWindowPositionInScreen(
const std::string& window_id,
GetWindowPositionInScreenCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run(std::nullopt);
return;
}
std::move(cb).Run(window->GetBoundsInScreen().origin());
}
void TestControllerAsh::LaunchAppFromAppList(const std::string& app_id) {
ash::Shell::Get()->app_list_controller()->ActivateItem(
app_id, /*event_flags=*/0, ash::AppListLaunchedFrom::kLaunchedFromGrid,
/*is_above_the_fold=*/false);
}
void TestControllerAsh::AreDesksBeingModified(
AreDesksBeingModifiedCallback callback) {
std::move(callback).Run(ash::DesksController::Get()->AreDesksBeingModified());
}
void TestControllerAsh::PinOrUnpinItemInShelf(
const std::string& item_id,
bool pin,
PinOrUnpinItemInShelfCallback callback) {
int item_index = ash::ShelfModel::Get()->ItemIndexByAppID(item_id);
if (item_index == -1) {
std::move(callback).Run(/*success=*/false);
return;
}
if (pin) {
ash::ShelfModel::Get()->PinExistingItemWithID(item_id);
} else {
ash::ShelfModel::Get()->UnpinAppWithID(item_id);
}
std::move(callback).Run(/*success=*/true);
}
void TestControllerAsh::ReinitializeAppService(
ReinitializeAppServiceCallback callback) {
Profile* const profile = ProfileManager::GetPrimaryUserProfile();
apps::AppServiceProxyFactory::GetForProfile(profile)->ReinitializeForTesting(
profile);
std::move(callback).Run();
}
void TestControllerAsh::SelectItemInShelf(const std::string& item_id,
SelectItemInShelfCallback callback) {
ash::ShelfItemDelegate* delegate =
ash::ShelfModel::Get()->GetShelfItemDelegate(ash::ShelfID(item_id));
if (!delegate) {
std::move(callback).Run(/*success=*/false);
return;
}
auto mouse_event = std::make_unique<ui::MouseEvent>(
ui::EventType::kMousePressed, gfx::PointF(), gfx::PointF(),
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
ui::EF_LEFT_MOUSE_BUTTON);
delegate->ItemSelected(std::move(mouse_event), display::kInvalidDisplayId,
ash::LAUNCH_FROM_SHELF,
/*callback=*/base::DoNothing(),
/*filter_predicate=*/base::NullCallback());
std::move(callback).Run(/*success=*/true);
}
void TestControllerAsh::SelectContextMenuForShelfItem(
const std::string& item_id,
uint32_t index,
SelectContextMenuForShelfItemCallback callback) {
ash::ShelfItemDelegate* delegate =
ash::ShelfModel::Get()->GetShelfItemDelegate(ash::ShelfID(item_id));
if (!delegate) {
std::move(callback).Run(false);
return;
}
delegate->GetContextMenu(
/*display_id=*/0,
base::BindOnce(&TestControllerAsh::OnSelectContextMenuForShelfItem,
std::move(callback), item_id, index));
}
void TestControllerAsh::SendTouchEvent(const std::string& window_id,
mojom::TouchEventType type,
uint8_t pointer_id,
const gfx::PointF& location_in_window,
SendTouchEventCallback cb) {
aura::Window* window = GetShellSurfaceWindow(window_id);
if (!window) {
std::move(cb).Run();
return;
}
// Newer lacros might send an enum we don't know about.
if (!mojom::IsKnownEnumValue(type)) {
LOG(WARNING) << "Unknown event type: " << type;
std::move(cb).Run();
return;
}
ui::EventType event_type;
switch (type) {
case mojom::TouchEventType::kUnknown:
// |type| is not optional, so kUnknown is never expected.
NOTREACHED_IN_MIGRATION();
return;
case mojom::TouchEventType::kPressed:
event_type = ui::EventType::kTouchPressed;
break;
case mojom::TouchEventType::kMoved:
event_type = ui::EventType::kTouchMoved;
break;
case mojom::TouchEventType::kReleased:
event_type = ui::EventType::kTouchReleased;
break;
case mojom::TouchEventType::kCancelled:
event_type = ui::EventType::kTouchCancelled;
break;
}
// Compute location relative to display root window.
gfx::PointF location_in_root(location_in_window);
aura::Window::ConvertPointToTarget(window, window->GetRootWindow(),
&location_in_root);
ui::PointerDetails details(ui::EventPointerType::kTouch, pointer_id, 1.0f,
1.0f, 0.0f);
ui::TouchEvent touch_event(event_type, location_in_window, location_in_root,
ui::EventTimeForNow(), details);
Dispatch(window->GetHost(), &touch_event);
std::move(cb).Run();
}
void TestControllerAsh::RegisterStandaloneBrowserTestController(
mojo::PendingRemote<mojom::StandaloneBrowserTestController> controller) {
// At the moment only a single controller is supported.
// TODO(crbug.com/40167449): Support SxS lacros.
if (standalone_browser_test_controller_.is_bound()) {
return;
}
standalone_browser_test_controller_.Bind(std::move(controller));
standalone_browser_test_controller_.set_disconnect_handler(base::BindOnce(
&TestControllerAsh::OnControllerDisconnected, base::Unretained(this)));
if (!on_standalone_browser_test_controller_bound_.is_signaled())
on_standalone_browser_test_controller_bound_.Signal();
}
void TestControllerAsh::WaiterFinished(OverviewWaiter* waiter) {
for (size_t i = 0; i < overview_waiters_.size(); ++i) {
if (waiter == overview_waiters_[i].get()) {
std::unique_ptr<OverviewWaiter> overview_waiter =
std::move(overview_waiters_[i]);
overview_waiters_.erase(overview_waiters_.begin() + i);
// Delete asynchronously to avoid re-entrancy. This is safe because the
// class will never use |test_controller_| after this callback.
base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(
FROM_HERE, std::move(overview_waiter));
break;
}
}
}
void TestControllerAsh::OnControllerDisconnected() {
standalone_browser_test_controller_.reset();
}
void TestControllerAsh::OnGetContextMenuForShelfItem(
GetContextMenuForShelfItemCallback callback,
std::unique_ptr<ui::SimpleMenuModel> model) {
std::vector<std::string> items;
items.reserve(model->GetItemCount());
for (size_t i = 0; i < model->GetItemCount(); ++i) {
items.push_back(base::UTF16ToUTF8(model->GetLabelAt(i)));
}
std::move(callback).Run(std::move(items));
}
void TestControllerAsh::OnSelectContextMenuForShelfItem(
SelectContextMenuForShelfItemCallback callback,
const std::string& item_id,
size_t index,
std::unique_ptr<ui::SimpleMenuModel> model) {
if (index < model->GetItemCount()) {
model->ActivatedAt(index, /*event_flags=*/0);
std::move(callback).Run(/*success=*/true);
return;
}
std::move(callback).Run(/*success=*/false);
}
void TestControllerAsh::GetOpenAshBrowserWindows(
GetOpenAshBrowserWindowsCallback callback) {
std::move(callback).Run(BrowserList::GetInstance()->size());
}
void TestControllerAsh::CloseAllBrowserWindows(
CloseAllBrowserWindowsCallback callback) {
for (Browser* browser : *BrowserList::GetInstance()) {
browser->window()->Close();
}
std::move(callback).Run(/*success*/ true);
}
void TestControllerAsh::TriggerTabScrubbing(
float x_offset,
TriggerTabScrubbingCallback callback) {
crosapi::BrowserManager::Get()->HandleTabScrubbing(x_offset, false);
// Return whether tab scrubbing logic has started or not in Ash.
//
// In practice, it is expected that it does not trigger the scrubbing logic,
// returning |false|, and signal Lacros to do so.
bool scrubbing = TabScrubberChromeOS::GetInstance()->IsActivationPending();
std::move(callback).Run(scrubbing);
}
void TestControllerAsh::SetSelectedSharesheetApp(
const std::string& app_id,
SetSelectedSharesheetAppCallback callback) {
sharesheet::SharesheetService::SetSelectedAppForTesting(
base::UTF8ToUTF16(app_id));
std::move(callback).Run();
}
void TestControllerAsh::GetAshVersion(GetAshVersionCallback callback) {
std::move(callback).Run(version_info::GetVersion().GetString());
}
void TestControllerAsh::BindTestShillController(
mojo::PendingReceiver<crosapi::mojom::TestShillController> receiver,
BindTestShillControllerCallback callback) {
mojo::MakeSelfOwnedReceiver<crosapi::mojom::TestShillController>(
std::make_unique<crosapi::TestShillControllerAsh>(), std::move(receiver));
std::move(callback).Run();
}
#if BUILDFLAG(USE_CUPS)
namespace {
// Observer that destroys itself after receiving OnPrintJobFinished event.
class SelfOwnedPrintJobHistoryServiceObserver
: public ash::PrintJobHistoryService::Observer {
public:
SelfOwnedPrintJobHistoryServiceObserver(
ash::PrintJobHistoryService* print_job_history_service,
base::OnceClosure on_print_job_finished)
: on_print_job_finished_(std::move(on_print_job_finished)) {
observation_.Observe(print_job_history_service);
}
~SelfOwnedPrintJobHistoryServiceObserver() override = default;
private:
// PrintJobHistoryService::Observer:
void OnPrintJobFinished(const ash::printing::proto::PrintJobInfo&) override {
observation_.Reset();
std::move(on_print_job_finished_).Run();
delete this;
}
base::ScopedObservation<ash::PrintJobHistoryService,
ash::PrintJobHistoryService::Observer>
observation_{this};
base::OnceClosure on_print_job_finished_;
};
} // namespace
#endif // BUILDFLAG(USE_CUPS)
// This class is set as UtteranceEventDelegate for the Ash TtsUtterance
// created in TestControllerAsh::TtsSpeak(), which is called from lacros browser
// test to simulate speaking an Ash utterance with a Lacros voice. It helps to
// verify that Tts events sent from Tts Speech Engine (in Lacros) are forwarded
// to its utterance event delegate in Ash.
class TestControllerAsh::AshUtteranceEventDelegate
: public content::UtteranceEventDelegate {
public:
AshUtteranceEventDelegate(
TestControllerAsh* controller,
mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> client)
: controller_(controller), client_(std::move(client)) {}
AshUtteranceEventDelegate(const AshUtteranceEventDelegate&) = delete;
AshUtteranceEventDelegate& operator=(const AshUtteranceEventDelegate&) =
delete;
~AshUtteranceEventDelegate() override = default;
// content::UtteranceEventDelegate:
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 Lacros, so that Lacros browser test can
// be notified that TtsEvent has been received by its UtteranceEventDelegate
// in Ash.
client_->OnTtsEvent(tts_crosapi_util::ToMojo(event_type), char_index,
char_length, error_message);
if (utterance->IsFinished()) {
controller_->OnAshUtteranceFinished(utterance->GetId());
// Note: |this| is deleted at this point.
}
}
private:
// |controller_| is guaranteed to be valid during the lifetime of this class.
const raw_ptr<TestControllerAsh> controller_;
mojo::Remote<crosapi::mojom::TtsUtteranceClient> client_;
};
void TestControllerAsh::CreateAndCancelPrintJob(
const std::string& job_title,
CreateAndCancelPrintJobCallback callback) {
#if BUILDFLAG(USE_CUPS)
auto* profile = ProfileManager::GetPrimaryUserProfile();
auto* observer = new SelfOwnedPrintJobHistoryServiceObserver(
ash::PrintJobHistoryServiceFactory::GetForBrowserContext(profile),
std::move(callback));
DCHECK(observer);
std::unique_ptr<ash::CupsPrintJob> print_job =
std::make_unique<ash::CupsPrintJob>(
chromeos::Printer(), /*job_id=*/0, job_title, /*total_page_number=*/1,
::printing::PrintJob::Source::kPrintPreview,
/*source_id=*/"", ash::printing::proto::PrintSettings());
ash::CupsPrintJobManager* print_job_manager =
ash::CupsPrintJobManagerFactory::GetForBrowserContext(profile);
print_job->set_state(ash::CupsPrintJob::State::STATE_NONE);
print_job_manager->NotifyJobCreated(print_job->GetWeakPtr());
print_job->set_state(ash::CupsPrintJob::State::STATE_CANCELLED);
print_job_manager->NotifyJobCanceled(print_job->GetWeakPtr());
#endif // BUILDFLAG(USE_CUPS)
}
void TestControllerAsh::BindShillClientTestInterface(
mojo::PendingReceiver<crosapi::mojom::ShillClientTestInterface> receiver,
BindShillClientTestInterfaceCallback callback) {
mojo::MakeSelfOwnedReceiver<crosapi::mojom::ShillClientTestInterface>(
std::make_unique<crosapi::ShillClientTestInterfaceAsh>(),
std::move(receiver));
std::move(callback).Run();
}
void TestControllerAsh::GetSanitizedActiveUsername(
GetSanitizedActiveUsernameCallback callback) {
user_manager::UserManager* user_manager = user_manager::UserManager::Get();
user_manager::User* user = user_manager->GetActiveUser();
CHECK(user);
::user_data_auth::GetSanitizedUsernameRequest request;
request.set_username(
cryptohome::CreateAccountIdentifierFromAccountId(user->GetAccountId())
.account_id());
ash::CryptohomeMiscClient::Get()->GetSanitizedUsername(
request, base::BindOnce(
[](GetSanitizedActiveUsernameCallback callback,
std::optional<::user_data_auth::GetSanitizedUsernameReply>
result) {
CHECK(result.has_value());
std::move(callback).Run(result->sanitized_username());
},
std::move(callback)));
}
void TestControllerAsh::BindInputMethodTestInterface(
mojo::PendingReceiver<crosapi::mojom::InputMethodTestInterface> receiver,
BindInputMethodTestInterfaceCallback callback) {
mojo::MakeSelfOwnedReceiver<crosapi::mojom::InputMethodTestInterface>(
std::make_unique<crosapi::InputMethodTestInterfaceAsh>(),
std::move(receiver));
std::move(callback).Run();
}
void TestControllerAsh::GetTtsUtteranceQueueSize(
GetTtsUtteranceQueueSizeCallback callback) {
std::move(callback).Run(
tts_crosapi_util::GetTtsUtteranceQueueSizeForTesting());
}
void TestControllerAsh::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 TestControllerAsh::TtsSpeak(
crosapi::mojom::TtsUtterancePtr mojo_utterance,
mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient> utterance_client) {
std::unique_ptr<content::TtsUtterance> ash_utterance =
tts_crosapi_util::CreateUtteranceFromMojo(
mojo_utterance, /*should_always_be_spoken=*/false);
auto event_delegate = std::make_unique<AshUtteranceEventDelegate>(
this, std::move(utterance_client));
ash_utterance->SetEventDelegate(event_delegate.get());
ash_utterance_event_delegates_.emplace(ash_utterance->GetId(),
std ::move(event_delegate));
tts_crosapi_util::SpeakForTesting(std::move(ash_utterance));
}
void TestControllerAsh::IsSavedDeskStorageReady(
IsSavedDeskStorageReadyCallback callback) {
std::move(callback).Run(DesksClient::Get()->GetDeskModel()->IsReady());
}
void TestControllerAsh::SetAssistiveTechnologyEnabled(
crosapi::mojom::AssistiveTechnologyType at_type,
bool enabled) {
ash::AccessibilityManager* manager = ash::AccessibilityManager::Get();
switch (at_type) {
case crosapi::mojom::AssistiveTechnologyType::kChromeVox:
manager->EnableSpokenFeedback(enabled);
break;
case mojom::AssistiveTechnologyType::kSelectToSpeak:
manager->SetSelectToSpeakEnabled(enabled);
break;
case mojom::AssistiveTechnologyType::kSwitchAccess: {
// Don't show "are you sure you want to turn off switch access?" dialog
// during these tests, as it causes a side-effect for future tests run
// in series.
auto* controller = ash::AccessibilityController::Get();
controller->DisableSwitchAccessDisableConfirmationDialogTesting();
// Don't show the dialog saying Switch Access was enabled.
controller->DisableSwitchAccessEnableNotificationTesting();
// Set some Switch Access prefs so that the os://settings page is not
// opened (this is done if settings are not configured on first use):
manager->SetSwitchAccessKeysForTest(
{'1', 'A'}, ash::prefs::kAccessibilitySwitchAccessNextDeviceKeyCodes);
manager->SetSwitchAccessKeysForTest(
{'2', 'B'},
ash::prefs::kAccessibilitySwitchAccessSelectDeviceKeyCodes);
manager->SetSwitchAccessEnabled(enabled);
break;
}
case crosapi::mojom::AssistiveTechnologyType::kFocusHighlight: {
manager->SetFocusHighlightEnabled(enabled);
break;
}
case mojom::AssistiveTechnologyType::kUnknown:
LOG(ERROR) << "Cannot enable unknown AssistiveTechnologyType";
break;
}
}
void TestControllerAsh::GetAppListItemAttributes(
const std::string& item_id,
GetAppListItemAttributesCallback callback) {
auto* profile = ProfileManager::GetPrimaryUserProfile();
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
auto attributes = mojom::AppListItemAttributes::New();
if (const app_list::AppListSyncableService::SyncItem* sync_item =
app_list_syncable_service->GetSyncItem(item_id)) {
attributes->item_position = sync_item->item_ordinal.ToDebugString();
attributes->pin_position = sync_item->item_pin_ordinal.ToDebugString();
}
std::move(callback).Run(std::move(attributes));
}
void TestControllerAsh::SetAppListItemAttributes(
const std::string& item_id,
mojom::AppListItemAttributesPtr attributes,
SetAppListItemAttributesCallback callback) {
auto* profile = ProfileManager::GetPrimaryUserProfile();
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
AppListModelUpdater* app_list_model_updater =
app_list_syncable_service->GetModelUpdater();
app_list_model_updater->SetActive(true);
app_list_model_updater->SetItemPosition(
item_id, syncer::StringOrdinal(attributes->item_position));
if (auto ordinal = syncer::StringOrdinal(attributes->pin_position);
ordinal.IsValid()) {
app_list_syncable_service->SetPinPosition(item_id, ordinal,
/*pinned_by_policy=*/false);
} else {
app_list_syncable_service->RemovePinPosition(item_id);
}
std::move(callback).Run();
}
void TestControllerAsh::CloseAllAshBrowserWindowsAndConfirm(
CloseAllAshBrowserWindowsAndConfirmCallback callback) {
SelfOwnedAshBrowserWindowCloser* closer =
new SelfOwnedAshBrowserWindowCloser(std::move(callback));
closer->CloseAllBrowserWindows();
}
void TestControllerAsh::CheckAtLeastOneAshBrowserWindowOpen(
CheckAtLeastOneAshBrowserWindowOpenCallback callback) {
SelfOwnedAshBrowserWindowOpenWaiter* window_waiter =
new SelfOwnedAshBrowserWindowOpenWaiter(std::move(callback));
window_waiter->CheckIfAtLeastOneWindowOpen();
}
void TestControllerAsh::GetAllOpenTabURLs(GetAllOpenTabURLsCallback callback) {
std::vector<GURL> result;
for (Browser* browser : *BrowserList::GetInstance()) {
for (int i = 0; i < browser->tab_strip_model()->GetTabCount(); i++) {
result.emplace_back(browser->tab_strip_model()
->GetWebContentsAt(i)
->GetLastCommittedURL());
}
}
std::move(callback).Run(std::move(result));
}
void TestControllerAsh::SetAlmanacEndpointUrlForTesting(
const std::optional<std::string>& url_override,
SetAlmanacEndpointUrlForTestingCallback callback) {
apps::SetAlmanacEndpointUrlForTesting(url_override);
std::move(callback).Run();
}
void TestControllerAsh::IsToastShown(const std::string& toast_id,
IsToastShownCallback callback) {
std::move(callback).Run(ash::ToastManager::Get()->IsToastShown(toast_id));
}
void TestControllerAsh::OnAshUtteranceFinished(int utterance_id) {
// Delete the utterance event delegate object when the utterance is finished.
ash_utterance_event_delegates_.erase(utterance_id);
}
void TestControllerAsh::SnapWindow(const std::string& window_id,
mojom::SnapPosition position,
SnapWindowCallback callback) {
aura::Window* window = GetShellSurfaceWindow(window_id);
CHECK(window);
ash::SplitViewTestApi().SnapWindow(
window, mojo::ConvertTo<ash::SnapPosition>(position));
std::move(callback).Run();
}
void TestControllerAsh::IsShelfVisible(IsShelfVisibleCallback callback) {
std::move(callback).Run(ash::ShelfTestApi().IsVisible());
}
void TestControllerAsh::SetAppInstallDialogAutoAccept(
bool auto_accept,
SetAppInstallDialogAutoAcceptCallback callback) {
ash::app_install::AppInstallPageHandler::SetAutoAcceptForTesting(auto_accept);
std::move(callback).Run();
}
void TestControllerAsh::UpdateDisplay(int number_of_displays,
UpdateDisplayCallback callback) {
CHECK(number_of_displays > 0 && number_of_displays <= 8);
display::test::DisplayManagerTestApi display_manager(
ash::Shell::Get()->display_manager());
const auto current_display_info =
display_manager.GetInternalManagedDisplayInfo(
display_manager.SetFirstDisplayAsInternalDisplay());
std::vector<display::ManagedDisplayInfo> display_infos;
display_infos.push_back(current_display_info);
for (int i = 1; i < number_of_displays; i++) {
// This simulates a series of screens that are aligned next to each other on
// the x-axis.
display_infos.push_back(display::ManagedDisplayInfo::CreateFromSpecWithID(
base::StrCat({base::ToString(i * kSimulatedDisplayXResolution), "+0-",
base::ToString(kSimulatedDisplayXResolution), "x",
base::ToString(kSimulatedDisplayYResolution)}),
current_display_info.id() + i));
}
display_manager.UpdateDisplayWithDisplayInfoList(display_infos);
std::move(callback).Run();
}
void TestControllerAsh::EnableStatisticsProviderForTesting(
bool enable,
EnableStatisticsProviderForTestingCallback callback) {
ash::system::StatisticsProvider::SetTestProvider(
enable ? &fake_statistics_provider_ : nullptr);
std::move(callback).Run();
}
void TestControllerAsh::ClearAllMachineStatistics(
ClearAllMachineStatisticsCallback callback) {
fake_statistics_provider_.ClearAllMachineStatistics();
std::move(callback).Run();
}
void TestControllerAsh::SetMachineStatistic(
mojom::MachineStatisticKeyType key,
const std::string& value,
SetMachineStatisticCallback callback) {
std::string key_string = GetMachineStatisticKeyString(key);
if (!key_string.empty()) {
fake_statistics_provider_.SetMachineStatistic(key_string, value);
std::move(callback).Run(true);
} else {
LOG(WARNING) << "Unknown key for setting machine statistic";
std::move(callback).Run(false);
}
}
void TestControllerAsh::SetMinFlingVelocity(
float velocity,
SetMinFlingVelocityCallback callback) {
ui::GestureConfiguration::GetInstance()->set_min_fling_velocity(velocity);
std::move(callback).Run();
}
// This class waits for overview mode to either enter or exit and fires a
// callback. This class will fire the callback at most once.
class TestControllerAsh::OverviewWaiter : public ash::OverviewObserver {
public:
OverviewWaiter(bool wait_for_enter,
base::OnceClosure closure,
TestControllerAsh* test_controller)
: wait_for_enter_(wait_for_enter),
closure_(std::move(closure)),
test_controller_(test_controller) {
ash::Shell::Get()->overview_controller()->AddObserver(this);
}
OverviewWaiter(const OverviewWaiter&) = delete;
OverviewWaiter& operator=(const OverviewWaiter&) = delete;
~OverviewWaiter() override {
ash::Shell::Get()->overview_controller()->RemoveObserver(this);
}
// OverviewObserver:
void OnOverviewModeStartingAnimationComplete(bool canceled) override {
if (wait_for_enter_) {
if (closure_) {
std::move(closure_).Run();
DCHECK(test_controller_);
TestControllerAsh* controller = test_controller_;
test_controller_ = nullptr;
controller->WaiterFinished(this);
}
}
}
void OnOverviewModeEndingAnimationComplete(bool canceled) override {
if (!wait_for_enter_) {
if (closure_) {
std::move(closure_).Run();
DCHECK(test_controller_);
TestControllerAsh* controller = test_controller_;
test_controller_ = nullptr;
controller->WaiterFinished(this);
}
}
}
private:
// If true, waits for enter. Otherwise waits for exit.
const bool wait_for_enter_;
base::OnceClosure closure_;
// The test controller owns this object so is never invalid.
raw_ptr<TestControllerAsh> test_controller_;
};
TestShillControllerAsh::TestShillControllerAsh() {
ash::ShillProfileClient::Get()->GetTestInterface()->AddProfile(
"/network/test", ash::ProfileHelper::GetUserIdHashFromProfile(
ProfileManager::GetPrimaryUserProfile()));
}
TestShillControllerAsh::~TestShillControllerAsh() = default;
void TestShillControllerAsh::OnPacketReceived(
const std::string& extension_id,
const std::string& configuration_name,
const std::vector<uint8_t>& data) {
const std::string key = crosapi::VpnServiceForExtensionAsh::GetKey(
extension_id, configuration_name);
const std::string shill_key = shill::kObjectPathBase + key;
// On linux ShillThirdPartyVpnDriverClient is initialized as Fake and
// therefore exposes a testing interface.
auto* client = ash::ShillThirdPartyVpnDriverClient::Get()->GetTestInterface();
CHECK(client);
client->OnPacketReceived(shill_key,
std::vector<char>(data.begin(), data.end()));
}
void TestShillControllerAsh::OnPlatformMessage(
const std::string& extension_id,
const std::string& configuration_name,
uint32_t message) {
const std::string key = crosapi::VpnServiceForExtensionAsh::GetKey(
extension_id, configuration_name);
const std::string shill_key = shill::kObjectPathBase + key;
// On linux ShillThirdPartyVpnDriverClient is initialized as Fake and
// therefore exposes a testing interface.
auto* client = ash::ShillThirdPartyVpnDriverClient::Get()->GetTestInterface();
CHECK(client);
client->OnPlatformMessage(shill_key, message);
}
////////////
// ShillClientTestInterfaceAsh
ShillClientTestInterfaceAsh::ShillClientTestInterfaceAsh() = default;
ShillClientTestInterfaceAsh::~ShillClientTestInterfaceAsh() = default;
void ShillClientTestInterfaceAsh::AddDevice(const std::string& device_path,
const std::string& type,
const std::string& name,
AddDeviceCallback callback) {
auto* device_test = ash::ShillDeviceClient::Get()->GetTestInterface();
device_test->AddDevice(device_path, type, name);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::ClearDevices(ClearDevicesCallback callback) {
auto* device_test = ash::ShillDeviceClient::Get()->GetTestInterface();
device_test->ClearDevices();
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::SetDeviceProperty(
const std::string& device_path,
const std::string& name,
::base::Value value,
bool notify_changed,
SetDevicePropertyCallback callback) {
auto* device_test = ash::ShillDeviceClient::Get()->GetTestInterface();
device_test->SetDeviceProperty(device_path, name, value, notify_changed);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::SetSimLocked(const std::string& device_path,
bool enabled,
SetSimLockedCallback callback) {
auto* device_test = ash::ShillDeviceClient::Get()->GetTestInterface();
device_test->SetSimLocked(device_path, enabled);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::AddService(
const std::string& service_path,
const std::string& guid,
const std::string& name,
const std::string& type,
const std::string& state,
bool visible,
SetDevicePropertyCallback callback) {
auto* service_test = ash::ShillServiceClient::Get()->GetTestInterface();
service_test->AddService(service_path, guid, name, type, state, visible);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::ClearServices(
ClearServicesCallback callback) {
auto* service_test = ash::ShillServiceClient::Get()->GetTestInterface();
service_test->ClearServices();
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::SetServiceProperty(
const std::string& service_path,
const std::string& property,
base::Value value,
SetServicePropertyCallback callback) {
auto* service_test = ash::ShillServiceClient::Get()->GetTestInterface();
service_test->SetServiceProperty(service_path, property, value);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::AddProfile(const std::string& profile_path,
const std::string& userhash,
AddProfileCallback callback) {
auto* profile_test = ash::ShillProfileClient::Get()->GetTestInterface();
profile_test->AddProfile(profile_path, userhash);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::AddServiceToProfile(
const std::string& profile_path,
const std::string& service_path,
AddServiceToProfileCallback callback) {
auto* profile_test = ash::ShillProfileClient::Get()->GetTestInterface();
profile_test->AddService(profile_path, service_path);
std::move(callback).Run();
}
void ShillClientTestInterfaceAsh::AddIPConfig(const std::string& ip_config_path,
::base::Value properties,
AddIPConfigCallback callback) {
auto* ip_config_test = ash::ShillIPConfigClient::Get()->GetTestInterface();
ip_config_test->AddIPConfig(ip_config_path, std::move(properties).TakeDict());
std::move(callback).Run();
}
} // namespace crosapi