chromium/chrome/browser/ash/crosapi/test_controller_ash.cc

// 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