chromium/chrome/browser/ash/app_list/app_service/app_service_promise_app_item_browsertest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_list/app_service/app_service_promise_app_item.h"

#include <memory>
#include <optional>
#include <ranges>
#include <string>
#include <vector>

#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/components/arc/arc_features.h"
#include "ash/components/arc/mojom/app.mojom.h"
#include "ash/components/arc/session/connection_holder.h"
#include "ash/components/arc/test/arc_util_test_support.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_app_instance.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_types.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_base.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_metrics.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_service.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.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/app_list/app_list_test_util.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_app_test.h"
#include "chrome/browser/ash/app_list/arc/arc_package_install_priority_handler.h"
#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
#include "chrome/browser/ash/app_list/chrome_app_list_model_updater.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/shelf_controller_helper.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/sync/model/string_ordinal.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/test/fake_sync_change_processor.h"
#include "components/sync/test/sync_change_processor_wrapper_for_test.h"
#include "content/public/test/browser_test.h"
#include "net/http/http_status_code.h"
#include "ui/base/models/menu_model.h"

namespace apps {

const apps::PackageId kTestPackageId =
    apps::PackageId(apps::PackageType::kArc, "com.test.package");

ash::AppListItem* GetAppListItem(const std::string& id) {
  return ash::AppListModelProvider::Get()->model()->FindItem(id);
}

bool IsItemPinned(const std::string& item_id) {
  const auto& shelf_items = ash::ShelfModel::Get()->items();
  auto pinned_item =
      base::ranges::find_if(shelf_items, [&item_id](const auto& shelf_item) {
        return shelf_item.id.app_id == item_id;
      });
  return pinned_item != std::ranges::end(shelf_items);
}

class AppServicePromiseAppItemBrowserTest
    : public extensions::PlatformAppBrowserTest,
      public PromiseAppRegistryCache::Observer {
 public:
  AppServicePromiseAppItemBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        {ash::features::kPromiseIcons, arc::kSyncInstallPriority}, {});
  }
  ~AppServicePromiseAppItemBrowserTest() override = default;

  // extensions::PlatformAppBrowserTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    arc::SetArcAvailableCommandLineForTesting(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    arc::ArcSessionManager::SetUiEnabledForTesting(false);
    extensions::PlatformAppBrowserTest::SetUpInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    extensions::PlatformAppBrowserTest::SetUpOnMainThread();

    arc::SetArcPlayStoreEnabledForProfile(profile(), true);

    arc_app_list_pref_ = ArcAppListPrefs::Get(profile());
    DCHECK(arc_app_list_pref_);

    base::RunLoop run_loop;
    arc_app_list_pref_->SetDefaultAppsReadyCallback(run_loop.QuitClosure());
    run_loop.Run();

    app_instance_ = std::make_unique<arc::FakeAppInstance>(arc_app_list_pref_);
    arc_app_list_pref_->app_connection_holder()->SetInstance(
        app_instance_.get());
    WaitForInstanceReady(arc_app_list_pref_->app_connection_holder());

    AppListClientImpl* client = AppListClientImpl::GetInstance();
    ASSERT_TRUE(client);
    client->UpdateProfile();
    cache_ = app_service_proxy()->PromiseAppRegistryCache();

    https_server_.RegisterRequestHandler(
        base::BindRepeating(&AppServicePromiseAppItemBrowserTest::HandleRequest,
                            base::Unretained(this)));
    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(https_server_.Start());
    base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
        ash::switches::kAlmanacApiUrl, https_server_.GetURL("/").spec());
  }

  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
      const net::test_server::HttpRequest& request) {
    // Mock a response to ensure that the test does not stay hanging for an
    // Almanac response. It will be a failure response so the promise app will
    // fall back to a placeholder icon.
    if (base::Contains(request.relative_url, "v1/promise-app/")) {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_INTERNAL_SERVER_ERROR);
      response->set_content_type("application/x-protobuf");
      response->set_content("");
      return response;
    }
    return nullptr;
  }

  void TearDownOnMainThread() override {
    app_list_syncable_service()->StopSyncing(syncer::APP_LIST);
    arc_app_list_pref_->app_connection_holder()->CloseInstance(
        app_instance_.get());
    app_instance_.reset();
    arc::ArcSessionManager::Get()->Shutdown();
    extensions::PlatformAppBrowserTest::TearDownOnMainThread();
  }

  ChromeAppListModelUpdater* GetChromeAppListModelUpdater() {
    return static_cast<ChromeAppListModelUpdater*>(
        app_list_syncable_service()->GetModelUpdater());
  }

  ChromeAppListItem* GetChromeAppListItem(const std::string& app_id) {
    AppListModelUpdater* model_updater =
        app_list_syncable_service()->GetModelUpdater();
    return model_updater->FindItem(app_id);
  }

  ChromeAppListItem* GetChromeAppListItem(const PackageId& package_id) {
    return GetChromeAppListItem(package_id.ToString());
  }

  apps::PromiseAppRegistryCache* cache() { return cache_; }

  apps::AppServiceProxy* app_service_proxy() {
    return apps::AppServiceProxyFactory::GetForProfile(profile());
  }

  ArcAppListPrefs* arc_app_list_pref() { return arc_app_list_pref_; }
  arc::FakeAppInstance* app_instance() { return app_instance_.get(); }

  app_list::AppListSyncableService* app_list_syncable_service() {
    return app_list::AppListSyncableServiceFactory::GetForProfile(profile());
  }

  void AddArcPackageWithApps(const std::string& package,
                             const std::vector<std::string>& activities) {
    for (const auto& activity : activities) {
      arc::mojom::AppInfoPtr app =
          arc::mojom::AppInfo::New("Fake App", package, activity, false);
      app_instance_->SendAppAdded(*app);
    }

    app_instance_->SendPackageAdded(arc::mojom::ArcPackageInfo::New(
        package,
        /*package_version=*/1, /*last_backup_android_id=*/1,
        /*last_backup_time=*/0, /*sync=*/false));
  }

  // Set the number of updates we expect the Promise App Registry Cache to
  // receive in the test.
  void ExpectNumUpdates(int num_updates) {
    expected_num_updates_ = num_updates;
    current_num_updates_ = 0;
    if (!obs_.IsObserving()) {
      obs_.Observe(cache_);
    }
  }

  void WaitForPromiseAppUpdates() {
    if (expected_num_updates_ == current_num_updates_) {
      return;
    }
    wait_run_loop_ = std::make_unique<base::RunLoop>();
    wait_run_loop_->Run();
  }

  // apps::PromiseAppRegistryCache::Observer:
  void OnPromiseAppUpdate(const PromiseAppUpdate& update) override {
    current_num_updates_++;
    if (wait_run_loop_ && wait_run_loop_->running() &&
        expected_num_updates_ == current_num_updates_) {
      wait_run_loop_->Quit();
    }
  }

  void OnPromiseAppRegistryCacheWillBeDestroyed(
      apps::PromiseAppRegistryCache* cache) override {
    obs_.Reset();
  }

  void SetUpSyncedArcPromiseApp(const std::string& package_name) {
    auto* install_priority_handler =
        arc_app_list_pref()->GetInstallPriorityHandler();
    DCHECK(install_priority_handler);
    install_priority_handler->InstallSyncedPacakge(
        package_name, arc::mojom::InstallPriority::kLow);
    // Test:
    // 1) Start the installation.
    // Expect 2 updates: Promise app registration, then Almanac response update.
    // Note: As the Almanac response is not mocked, the promise icon will
    // fallback to using a placeholder image.
    ExpectNumUpdates(/*num_updates=*/2);
    app_instance()->SendInstallationStarted(package_name);
    WaitForPromiseAppUpdates();
  }

 private:
  raw_ptr<apps::PromiseAppRegistryCache, DanglingUntriaged> cache_;
  raw_ptr<ArcAppListPrefs, DanglingUntriaged> arc_app_list_pref_ = nullptr;

  std::unique_ptr<arc::FakeAppInstance> app_instance_;

  base::ScopedObservation<PromiseAppRegistryCache,
                          PromiseAppRegistryCache::Observer>
      obs_{this};

  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<base::RunLoop> wait_run_loop_;
  net::EmbeddedTestServer https_server_;

  // Tracks how many times we should expect OnPromiseAppUpdate to be called
  // before proceeding with a test.
  int expected_num_updates_;
  int current_num_updates_;
};

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       ShouldShowUpdateCreatesItem) {
  // Sync setup.
  std::unique_ptr<syncer::FakeSyncChangeProcessor> sync_processor =
      std::make_unique<syncer::FakeSyncChangeProcessor>();
  app_list::AppListSyncableService* app_list_syncable_service_ =
      app_list_syncable_service();
  app_list_syncable_service_->MergeDataAndStartSyncing(
      syncer::APP_LIST, {},
      std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(
          sync_processor.get()));
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app registration in the cache should not result in a promise app
  // launcher item if should_show is false (which it is by default).
  ash::AppListItem* item = GetAppListItem(kTestPackageId.ToString());
  ASSERT_FALSE(item);

  // Update the promise app to allow showing in the Launcher.
  apps::PromiseAppPtr promise_app_update =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app_update->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app_update));

  // Promise app item should now exist in the model.
  item = GetAppListItem(kTestPackageId.ToString());
  ASSERT_TRUE(item);

  // Verify that the promise app item is not added to local storage.
  const base::Value::Dict& local_items =
      profile()->GetPrefs()->GetDict(prefs::kAppListLocalState);
  const base::Value::Dict* dict_item =
      local_items.FindDict(kTestPackageId.ToString());
  EXPECT_FALSE(dict_item);

  // Verify that promise app item is not uploaded to sync data.
  for (auto sync_change : sync_processor->changes()) {
    const std::string item_id =
        sync_change.sync_data().GetSpecifics().app_list().item_id();
    EXPECT_NE(item_id, kTestPackageId.ToString());
  }
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       PromiseAppItemContextMenu) {
  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  ASSERT_EQ(item->name(),
            base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
                apps::PromiseStatus::kPending)));

  // Retrieve the context menu.
  base::RunLoop run_loop;
  std::unique_ptr<ui::SimpleMenuModel> menu_model;
  item->GetContextMenuModel(
      ash::AppListItemContext::kAppsGrid,
      base::BindLambdaForTesting(
          [&](std::unique_ptr<ui::SimpleMenuModel> created_menu) {
            menu_model = std::move(created_menu);
            run_loop.Quit();
          }));
  run_loop.Run();

  // The context menu should have the option to pin to shelf, a separator and
  // the reorder submenu.
  EXPECT_EQ(menu_model->GetItemCount(), 3u);
  EXPECT_EQ(menu_model->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
  EXPECT_EQ(menu_model->GetCommandIdAt(0), ash::CommandId::TOGGLE_PIN);

  EXPECT_EQ(menu_model->GetTypeAt(1), ui::MenuModel::ItemType::TYPE_SEPARATOR);

  EXPECT_EQ(menu_model->GetTypeAt(2), ui::MenuModel::ItemType::TYPE_SUBMENU);
  EXPECT_EQ(menu_model->GetCommandIdAt(2), ash::CommandId::REORDER_SUBMENU);

  // Reorder context menu should have options to reorder alphabetically and by
  // color.
  auto* reorder_submenu = menu_model->GetSubmenuModelAt(2);
  ASSERT_EQ(reorder_submenu->GetItemCount(), 2u);
  EXPECT_EQ(reorder_submenu->GetCommandIdAt(0),
            ash::CommandId::REORDER_BY_NAME_ALPHABETICAL);
  EXPECT_EQ(reorder_submenu->GetCommandIdAt(1),
            ash::CommandId::REORDER_BY_COLOR);
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       CompleteAppInstallationRemovesPromiseAppItem) {
  AppType app_type = AppType::kArc;
  std::string identifier = "test.com.example";
  PackageId package_id(PackageType::kArc, identifier);

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ash::AppListItem* item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(item);

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  std::string app_id = "qwertyuiopasdfghjkl";
  apps::AppPtr app = std::make_unique<apps::App>(app_type, app_id);
  app->publisher_id = identifier;
  app->readiness = apps::Readiness::kReady;
  std::vector<apps::AppPtr> apps;
  apps.push_back(std::move(app));
  app_service_proxy()->OnApps(std::move(apps), app_type,
                              /*should_notify_initialized=*/false);

  // Promise app item should no longer exist in the model.
  item = GetAppListItem(package_id.ToString());
  ASSERT_FALSE(item);
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       UpdatedFieldsShowInChromeAppListItem) {
  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->status = PromiseStatus::kPending;
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->progress(), 0);
  EXPECT_EQ(item->app_status(), ash::AppStatus::kPending);
  ASSERT_EQ(item->name(),
            base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
                apps::PromiseStatus::kPending)));

  // Update the promise app in the promise app registry cache.
  apps::PromiseAppPtr update = std::make_unique<PromiseApp>(kTestPackageId);
  update->progress = 0.3;
  update->status = PromiseStatus::kInstalling;
  cache()->OnPromiseApp(std::move(update));

  // Promise app item should have updated fields.
  EXPECT_EQ(item->progress(), 0.3f);
  EXPECT_EQ(item->app_status(), ash::AppStatus::kInstalling);
  EXPECT_EQ(item->name(),
            base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
                apps::PromiseStatus::kInstalling)));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest, SetToSyncPosition) {
  syncer::StringOrdinal ordinal = syncer::StringOrdinal::CreateInitialOrdinal();

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  // Add entry in sync data that has a matching PackageId with the promise app.
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      app_id, "App Name", /*parent_id=*/std::string(),
      ordinal.ToInternalValue(), /*item_pin_ordinal=*/std::string(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP,
      /*is_user_pinned=*/std::nullopt,
      /*promise_package_id=*/kTestPackageId.ToString())));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model at the correct position.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->position(), ordinal);

  syncer::StringOrdinal ordinal_after_sync = ordinal.CreateAfter();
  syncer::SyncChangeList change_list;
  change_list.push_back(syncer::SyncChange(
      FROM_HERE, syncer::SyncChange::ACTION_ADD,
      app_list::CreateAppRemoteData(
          app_id, "Test App", "", ordinal_after_sync.ToInternalValue(), "",
          sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt,
          kTestPackageId.ToString())));
  app_list_syncable_service()->ProcessSyncChanges(base::Location(),
                                                  change_list);

  // Verify the promise package position gets updaed by sync.
  item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->position(), ordinal_after_sync);

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  EXPECT_FALSE(GetChromeAppListItem(kTestPackageId));
  ChromeAppListItem* app_item = GetChromeAppListItem(app_id);
  ASSERT_TRUE(app_item);
  EXPECT_EQ(app_item->position(), ordinal_after_sync);
  EXPECT_FALSE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       InstalledAppTakesPromiseAppPosition) {
  syncer::StringOrdinal initial_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal app_ordinal = initial_ordinal.CreateAfter();

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  // Add entry in sync data that has a matching PackageId with the promise app.
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      "different.app", "Other app", /*parent_id=*/std::string(),
      initial_ordinal.ToInternalValue(), /*item_pin_ordinal=*/std::string(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP)));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);

  GetChromeAppListModelUpdater()->RequestPositionUpdate(
      kTestPackageId.ToString(), app_ordinal,
      ash::RequestPositionUpdateReason::kMoveItem);
  EXPECT_EQ(item->position(), app_ordinal);

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  EXPECT_FALSE(GetChromeAppListItem(kTestPackageId));
  ChromeAppListItem* app_item = GetChromeAppListItem(app_id);
  ASSERT_TRUE(app_item);
  EXPECT_EQ(app_item->position(), app_ordinal);
  EXPECT_TRUE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest, SetToSyncParent) {
  syncer::StringOrdinal folder_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal item_ordinal = folder_ordinal.CreateAfter();

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  const std::string kFolderItemId = "folder_id";
  syncer::SyncDataList sync_list;
  sync_list.push_back(app_list::CreateAppRemoteData(
      kFolderItemId, "Folder", "", folder_ordinal.ToInternalValue(), "",
      sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER));
  sync_list.push_back(app_list::CreateAppRemoteData(
      app_id, "App name", kFolderItemId, item_ordinal.ToInternalValue(),
      /*item_pin_ordinal=*/std::string(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP,
      /*is_user_pinned=*/std::nullopt,
      /*promise_package_id=*/kTestPackageId.ToString()));

  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model within the folder specified in
  // sync data.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->folder_id(), kFolderItemId);

  syncer::SyncChangeList change_list;
  change_list.push_back(syncer::SyncChange(
      FROM_HERE, syncer::SyncChange::ACTION_ADD,
      app_list::CreateAppRemoteData(
          app_id, "App name", "", item_ordinal.ToInternalValue(), "",
          sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt,
          kTestPackageId.ToString())));
  app_list_syncable_service()->ProcessSyncChanges(base::Location(),
                                                  change_list);

  // Verify the promise package position gets updaed by sync.
  item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->folder_id(), "");

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  EXPECT_FALSE(GetChromeAppListItem(kTestPackageId));
  ChromeAppListItem* app_item = GetChromeAppListItem(app_id);
  ASSERT_TRUE(app_item);
  EXPECT_EQ(app_item->folder_id(), "");
  EXPECT_FALSE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       InstalledAppTakesPromiseAppParent) {
  syncer::StringOrdinal folder_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal other_app_ordinal = folder_ordinal.CreateAfter();

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  const std::string kFolderItemId = "folder_id";

  const std::string other_package = "test.com.other";
  const std::string other_app_activity = "test.com.other.app";
  const std::string other_app_id =
      ArcAppListPrefs::GetAppId(other_package, other_app_activity);

  syncer::SyncDataList sync_list;
  sync_list.push_back(app_list::CreateAppRemoteData(
      kFolderItemId, "Folder", "", folder_ordinal.ToInternalValue(), "",
      sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER));
  sync_list.push_back(app_list::CreateAppRemoteData(
      other_app_id, "Other app", kFolderItemId,
      other_app_ordinal.ToInternalValue(),
      /*item_pin_ordinal=*/std::string(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  AddArcPackageWithApps(other_package, {other_app_activity});

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model at the correct position.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);

  GetChromeAppListModelUpdater()->RequestMoveItemToFolder(
      kTestPackageId.ToString(), kFolderItemId);
  EXPECT_EQ(item->folder_id(), kFolderItemId);

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  EXPECT_FALSE(GetChromeAppListItem(kTestPackageId));
  ChromeAppListItem* app_item = GetChromeAppListItem(app_id);
  ASSERT_TRUE(app_item);
  EXPECT_EQ(app_item->folder_id(), kFolderItemId);
  EXPECT_TRUE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       LabelMatchesWithStatus) {
  // Register test promise app.
  PromiseAppPtr promise_app = std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->status = PromiseStatus::kPending;
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should now exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  ASSERT_EQ(item->app_status(), ash::AppStatus::kPending);
  ASSERT_EQ(item->name(),
            base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
                PromiseStatus::kPending)));

  // Push a status update to the promise app.
  PromiseAppPtr update = std::make_unique<PromiseApp>(kTestPackageId);
  update->status = PromiseStatus::kInstalling;
  cache()->OnPromiseApp(std::move(update));

  // Item should now reflect the new status and name.
  EXPECT_TRUE(item);
  EXPECT_EQ(item->app_status(), ash::AppStatus::kInstalling);
  EXPECT_EQ(item->name(),
            base::UTF16ToUTF8(ShelfControllerHelper::GetLabelForPromiseStatus(
                PromiseStatus::kInstalling)));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       InstalledAppPinnedWhenPinningPromiseApp) {
  std::string identifier = "test.com.example";
  PackageId package_id(PackageType::kArc, identifier);

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ash::AppListItem* item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(item);

  // Pin the promise app to shelf.
  AppListClientImpl::GetInstance()->PinApp(package_id.ToString());
  EXPECT_TRUE(IsItemPinned(package_id.ToString()));

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  std::string app_activity = "test.com.example.activity";
  AddArcPackageWithApps(identifier, {app_activity});

  // Promise app item should no longer exist in the model.
  item = GetAppListItem(package_id.ToString());
  ASSERT_FALSE(item);
  EXPECT_FALSE(IsItemPinned(package_id.ToString()));

  // Verify that the app installed in place of the promise app is pinned.
  const std::string installed_app_id =
      ArcAppListPrefs::GetAppId(identifier, app_activity);
  EXPECT_TRUE(IsItemPinned(installed_app_id));

  ash::AppListItem* app_item = GetAppListItem(installed_app_id);
  ASSERT_TRUE(app_item);
  EXPECT_TRUE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       PromiseAppPinnedIfLinkedToAPinnedSyncedApp) {
  syncer::StringOrdinal ordinal = syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal pin_ordinal = ordinal.CreateAfter();

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);
  // Add entry in sync data that has a matching PackageId with the promise app.
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      app_id, "App Name", /*parent_id=*/std::string(),
      ordinal.ToInternalValue(), pin_ordinal.ToInternalValue(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP,
      /*is_user_pinned=*/std::nullopt,
      /*promise_package_id=*/kTestPackageId.ToString())));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model, and be pinned.
  const std::string promise_app_id = kTestPackageId.ToString();
  ash::AppListItem* item = GetAppListItem(promise_app_id);
  ASSERT_TRUE(item);

  EXPECT_TRUE(IsItemPinned(promise_app_id));

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  // Promise app item should no longer exist in the model.
  item = GetAppListItem(promise_app_id);
  ASSERT_FALSE(item);
  EXPECT_FALSE(IsItemPinned(promise_app_id));

  const std::string installed_app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);
  // Verify that the app installed in place of the promise app is pinned.
  EXPECT_TRUE(IsItemPinned(installed_app_id));

  ash::AppListItem* app_item = GetAppListItem(installed_app_id);
  ASSERT_TRUE(app_item);
  EXPECT_FALSE(app_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       PromiseItemInstallsMultipleApps) {
  syncer::StringOrdinal ordinal = syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal pin_ordinal = ordinal.CreateAfter();
  syncer::StringOrdinal promise_app_ordinal = pin_ordinal.CreateAfter();

  const std::string app_activity_in_sync = "test.com.example.activity.1";
  const std::string app_id_in_sync = ArcAppListPrefs::GetAppId(
      kTestPackageId.identifier(), app_activity_in_sync);
  // Add entry in sync data that has a matching PackageId with the promise app.
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      app_id_in_sync, "App Name", /*parent_id=*/std::string(),
      ordinal.ToInternalValue(), std::string(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP,
      /*is_user_pinned=*/std::nullopt,
      /*promise_package_id=*/kTestPackageId.ToString())));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model, and be pinned.
  const std::string promise_app_id = kTestPackageId.ToString();
  ash::AppListItem* item = GetAppListItem(promise_app_id);
  ASSERT_TRUE(item);

  GetChromeAppListModelUpdater()->RequestPositionUpdate(
      promise_app_id, promise_app_ordinal,
      ash::RequestPositionUpdateReason::kMoveItem);

  AppListClientImpl::GetInstance()->PinApp(promise_app_id);
  EXPECT_TRUE(IsItemPinned(promise_app_id));

  // Register (i.e. "install") an app with a matching package ID. This should
  // trigger removal of the promise app.
  std::string extra_app_activity = "test.com.example.activity.2";
  AddArcPackageWithApps(kTestPackageId.identifier(),
                        {extra_app_activity, app_activity_in_sync});

  // Promise app item should no longer exist in the model.
  item = GetAppListItem(promise_app_id);
  ASSERT_FALSE(item);
  EXPECT_FALSE(IsItemPinned(promise_app_id));

  const std::string extra_app_id = ArcAppListPrefs::GetAppId(
      kTestPackageId.identifier(), extra_app_activity);
  // Verify that the app installed in place of the promise app is pinned.
  EXPECT_TRUE(IsItemPinned(app_id_in_sync));
  EXPECT_FALSE(IsItemPinned(extra_app_id));

  // Verify the installed app positions in app list are as expected.
  const ash::AppListItem* const old_installed_item =
      GetAppListItem(app_id_in_sync);
  ASSERT_TRUE(old_installed_item);
  EXPECT_EQ(promise_app_ordinal, old_installed_item->position());
  EXPECT_FALSE(old_installed_item->is_new_install());

  const ash::AppListItem* const new_installed_item =
      GetAppListItem(extra_app_id);
  ASSERT_TRUE(new_installed_item);
  EXPECT_NE(promise_app_ordinal, new_installed_item->position());
  EXPECT_TRUE(new_installed_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       TargetItemSyncedWhileInstallingPromiseApp) {
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, syncer::SyncDataList(),
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model, and be pinned.
  const std::string promise_app_id = kTestPackageId.ToString();
  ash::AppListItem* item = GetAppListItem(promise_app_id);
  ASSERT_TRUE(item);

  AppListClientImpl::GetInstance()->PinApp(promise_app_id);
  EXPECT_TRUE(IsItemPinned(promise_app_id));

  syncer::StringOrdinal app_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal promise_app_ordinal = app_ordinal.CreateAfter();

  GetChromeAppListModelUpdater()->RequestPositionUpdate(
      promise_app_id, promise_app_ordinal,
      ash::RequestPositionUpdateReason::kMoveItem);

  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  syncer::SyncChangeList change_list;
  change_list.push_back(syncer::SyncChange(
      FROM_HERE, syncer::SyncChange::ACTION_ADD,
      app_list::CreateAppRemoteData(
          app_id, "Test App", "", app_ordinal.ToInternalValue(), "",
          sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt,
          kTestPackageId.ToString())));
  app_list_syncable_service()->ProcessSyncChanges(base::Location(),
                                                  change_list);
  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  // Promise app item should no longer exist in the model.
  item = GetAppListItem(promise_app_id);
  ASSERT_FALSE(item);
  EXPECT_FALSE(IsItemPinned(promise_app_id));

  // Verify that the app installed in place of the promise app is pinned.
  EXPECT_TRUE(IsItemPinned(app_id));

  const ash::AppListItem* const installed_item = GetAppListItem(app_id);
  ASSERT_TRUE(installed_item);
  EXPECT_EQ(promise_app_ordinal, installed_item->position());
  EXPECT_FALSE(installed_item->is_new_install());
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       MainLabelAndAccessibleLabelAreCorrect) {
  std::string app_name = "Long Name";

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->status = PromiseStatus::kPending;
  promise_app->name = app_name;
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->app_status(), ash::AppStatus::kPending);
  ASSERT_EQ(item->name(), "Waiting…");
  ASSERT_EQ(item->accessible_name(), "Long Name, waiting");

  // Update the promise app in the promise app registry cache.
  apps::PromiseAppPtr update = std::make_unique<PromiseApp>(kTestPackageId);
  update->progress = 0.3;
  update->status = PromiseStatus::kInstalling;
  cache()->OnPromiseApp(std::move(update));

  // Promise app item should have updated fields.
  EXPECT_EQ(item->app_status(), ash::AppStatus::kInstalling);
  EXPECT_EQ(item->name(), "Installing…");
  ASSERT_EQ(item->accessible_name(), "Long Name, installing");
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       PlaceholderAccessibleLabelUsedWhenNoNameAvailable) {
  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  EXPECT_EQ(item->app_status(), ash::AppStatus::kPending);
  ASSERT_EQ(item->accessible_name(), "An app, waiting");

  // Update the promise app in the promise app registry cache.
  apps::PromiseAppPtr update = std::make_unique<PromiseApp>(kTestPackageId);
  update->status = PromiseStatus::kInstalling;
  cache()->OnPromiseApp(std::move(update));

  // Promise app item should have updated fields.
  EXPECT_EQ(item->app_status(), ash::AppStatus::kInstalling);
  ASSERT_EQ(item->accessible_name(), "An app, installing");
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       LauncherItemCreationUpdatesMetrics) {
  base::HistogramTester histogram_tester;
  histogram_tester.ExpectBucketCount(
      kPromiseAppLifecycleEventHistogram,
      apps::PromiseAppLifecycleEvent::kCreatedInLauncher, 0);

  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  histogram_tester.ExpectBucketCount(
      kPromiseAppLifecycleEventHistogram,
      apps::PromiseAppLifecycleEvent::kCreatedInLauncher, 1);
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       ReinstallRemovedDefaultApp) {
  const std::string app_activity = "test.com.example.activity";
  const std::string app_id =
      ArcAppListPrefs::GetAppId(kTestPackageId.identifier(), app_activity);

  syncer::StringOrdinal ordinal = syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal pin_ordinal = ordinal.CreateAfter();
  // Add entry in sync data that has a matching PackageId with the promise app.
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      app_id, "App Name", /*parent_id=*/std::string(),
      ordinal.ToInternalValue(), pin_ordinal.ToInternalValue(),
      /*item_type=*/
      sync_pb::AppListSpecifics_AppListItemType_TYPE_REMOVE_DEFAULT_APP,
      /*is_user_pinned=*/std::nullopt,
      /*promise_package_id=*/kTestPackageId.ToString())));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Register a promise app in the promise app registry cache.
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  const std::string promise_app_id = kTestPackageId.ToString();
  ash::AppListItem* item = GetAppListItem(promise_app_id);
  ASSERT_TRUE(item);

  syncer::StringOrdinal promise_app_ordinal = pin_ordinal.CreateAfter();
  GetChromeAppListModelUpdater()->RequestPositionUpdate(
      promise_app_id, promise_app_ordinal,
      ash::RequestPositionUpdateReason::kMoveItem);
  AppListClientImpl::GetInstance()->PinApp(promise_app_id);

  AddArcPackageWithApps(kTestPackageId.identifier(), {app_activity});

  EXPECT_FALSE(GetAppListItem(promise_app_id));
  EXPECT_FALSE(IsItemPinned(promise_app_id));
  ash::AppListItem* app_item = GetAppListItem(app_id);
  ASSERT_TRUE(app_item);
  EXPECT_EQ(promise_app_ordinal, app_item->position());
  EXPECT_TRUE(IsItemPinned(app_id));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest, ContextMenu) {
  apps::PromiseAppPtr promise_app =
      std::make_unique<PromiseApp>(kTestPackageId);
  promise_app->should_show = true;
  cache()->OnPromiseApp(std::move(promise_app));

  // Promise app item should exist in the model.
  ChromeAppListItem* item = GetChromeAppListItem(kTestPackageId);
  ASSERT_TRUE(item);
  base::test::TestFuture<std::unique_ptr<ui::SimpleMenuModel>> future;
  item->GetContextMenuModel(ash::AppListItemContext::kNone,
                            future.GetCallback());
  std::unique_ptr<ui::SimpleMenuModel> menu_model = future.Take();

  // The context menu should only have the option to pin to shelf.
  EXPECT_EQ(menu_model->GetItemCount(), 1u);
  EXPECT_EQ(menu_model->GetTypeAt(0), ui::MenuModel::ItemType::TYPE_COMMAND);
  EXPECT_EQ(menu_model->GetCommandIdAt(0), ash::CommandId::TOGGLE_PIN);
}

// Test the full promise icon lifecycle where promise icon changes are triggered
// by ARC mojom updates.
IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       SyncedArcAppPromiseIconLifecycleInLauncherAndShelf) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);
  const std::string app_id =
      ArcAppListPrefs::GetAppId(package_name, activity_name);

  // Set Up: Add entry in sync data.
  syncer::StringOrdinal launcher_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncer::StringOrdinal shelf_ordinal = launcher_ordinal.CreateAfter();
  syncer::SyncDataList sync_list;
  sync_list.push_back((app_list::CreateAppRemoteData(
      app_id, app_name, /*parent_id=*/std::string(),
      launcher_ordinal.ToInternalValue(), shelf_ordinal.ToInternalValue(),
      /*item_type=*/sync_pb::AppListSpecifics_AppListItemType_TYPE_APP,
      /*is_user_pinned=*/true,
      /*promise_package_id=*/package_id.ToString())));
  app_list_syncable_service()->MergeDataAndStartSyncing(
      syncer::APP_LIST, sync_list,
      std::make_unique<syncer::FakeSyncChangeProcessor>());
  content::RunAllTasksUntilIdle();

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);

  // Test:
  // 1) Start the installation.
  // Expect 2 updates: Promise app registration, then Almanac response update.
  // Note: As the Almanac response is not mocked, the promise icon will fallback
  // to using a placeholder image.
  ExpectNumUpdates(/*num_updates=*/2);
  app_instance()->SendInstallationStarted(package_name);
  WaitForPromiseAppUpdates();

  // Confirm that the promise icon gets generated with the correct label and
  // icon in the positions indicated by the sync data.
  ash::AppListItem* launcher_item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(launcher_item);
  EXPECT_EQ(launcher_item->name(), "Waiting…");
  EXPECT_EQ(launcher_item->progress(), 0);
  EXPECT_EQ(launcher_item->position(), launcher_ordinal);
  EXPECT_TRUE(IsItemPinned(package_id.ToString()));
  auto* shelf_controller = ChromeShelfController::instance();
  const ash::ShelfItem* shelf_item =
      shelf_controller->GetItem(ash::ShelfID(package_id.ToString()));
  EXPECT_EQ(shelf_item->progress, 0);
  EXPECT_EQ(app_list_syncable_service()->GetPinPosition(package_id.ToString()),
            shelf_ordinal);

  // 2) Send a progress update.
  app_instance()->SendInstallationProgressChanged(package_name, 0.2);

  // Confirm the promise icon fields.
  launcher_item = GetAppListItem(package_id.ToString());
  EXPECT_EQ(launcher_item->name(), "Installing…");
  EXPECT_FLOAT_EQ(launcher_item->progress(), 0.2f);
  EXPECT_EQ(launcher_item->position(), launcher_ordinal);
  EXPECT_TRUE(IsItemPinned(package_id.ToString()));
  EXPECT_EQ(app_list_syncable_service()->GetPinPosition(package_id.ToString()),
            shelf_ordinal);
  shelf_item = shelf_controller->GetItem(ash::ShelfID(package_id.ToString()));
  EXPECT_FLOAT_EQ(shelf_item->progress, 0.2f);

  // 3) Finish the installation.
  std::vector<arc::mojom::AppInfoPtr> apps;
  arc::mojom::AppInfoPtr app_info = arc::mojom::AppInfo::New(
      app_name, package_name, activity_name, /*sticky=*/false);
  apps.emplace_back(std::move(app_info));
  app_instance()->SendRefreshAppList(apps);

  // Confirm that the promise icon no longer exists.
  launcher_item = GetAppListItem(package_id.ToString());
  EXPECT_FALSE(launcher_item);
  EXPECT_FALSE(IsItemPinned(package_id.ToString()));

  // Confirm that the installed app has replaced the promise icon in the correct
  // Launcher and Shelf position.
  ash::AppListItem* installed_launcher_item = GetAppListItem(app_id);
  EXPECT_TRUE(installed_launcher_item);
  EXPECT_EQ(installed_launcher_item->name(), app_name);
  EXPECT_EQ(installed_launcher_item->position(), launcher_ordinal);
  EXPECT_TRUE(IsItemPinned(app_id));
  EXPECT_EQ(app_list_syncable_service()->GetPinPosition(app_id), shelf_ordinal);
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       ActivatePromiseArcAppWhilePending) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);
  SetUpSyncedArcPromiseApp(package_name);

  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));

  ash::AppListItem* launcher_item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(launcher_item);
  EXPECT_EQ(launcher_item->name(), "Waiting…");

  ChromeAppListItem* item = GetChromeAppListItem(package_id);
  ASSERT_TRUE(item);
  item->Activate(ui::EF_NONE);

  ASSERT_EQ(arc::mojom::InstallPriority::kMedium,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       ActivatePromiseArcAppWhileInstalling) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);
  SetUpSyncedArcPromiseApp(package_name);

  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));

  ash::AppListItem* launcher_item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(launcher_item);
  EXPECT_EQ(launcher_item->name(), "Waiting…");

  // Send a progress update.
  app_instance()->SendInstallationProgressChanged(package_name, 0.2);

  // Confirm the promise icon fields.
  EXPECT_EQ(launcher_item->name(), "Installing…");

  ChromeAppListItem* item = GetChromeAppListItem(package_id);
  ASSERT_TRUE(item);
  item->Activate(ui::EF_NONE);

  // Install priority should not change if the installation has already started.
  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       SelectPromiseArcAppFromShelfWhilePending) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);
  SetUpSyncedArcPromiseApp(package_name);

  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));

  ash::ShelfModel* shelf_model = ash::ShelfModel::Get();
  PinAppWithIDToShelf(package_id.ToString());

  ash::ShelfItemDelegate* delegate =
      shelf_model->GetShelfItemDelegate(ash::ShelfID(package_id.ToString()));

  DCHECK(delegate);
  delegate->ItemSelected(/*event=*/nullptr, display::kInvalidDisplayId,
                         ash::LAUNCH_FROM_UNKNOWN,
                         /*callback=*/base::DoNothing(),
                         /*filter_predicate=*/base::NullCallback());

  ASSERT_EQ(arc::mojom::InstallPriority::kMedium,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       SelectPromiseArcAppFromShelfWhileInstalling) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);
  SetUpSyncedArcPromiseApp(package_name);

  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));

  // Send a progress update.
  app_instance()->SendInstallationProgressChanged(package_name, 0.2);

  ash::ShelfModel* shelf_model = ash::ShelfModel::Get();
  PinAppWithIDToShelf(package_id.ToString());

  ash::ShelfItemDelegate* delegate =
      shelf_model->GetShelfItemDelegate(ash::ShelfID(package_id.ToString()));

  DCHECK(delegate);
  delegate->ItemSelected(/*event=*/nullptr, display::kInvalidDisplayId,
                         ash::LAUNCH_FROM_UNKNOWN,
                         /*callback=*/base::DoNothing(),
                         /*filter_predicate=*/base::NullCallback());

  // Install priority should not be updated if the package installation has
  // started.
  ASSERT_EQ(arc::mojom::InstallPriority::kLow,
            arc_app_list_pref()
                ->GetInstallPriorityHandler()
                ->GetInstallPriorityForTesting(package_name));
}

IN_PROC_BROWSER_TEST_F(AppServicePromiseAppItemBrowserTest,
                       CheckArcPromiseAppIconWhenArcConnectionClosed) {
  // Test package details.
  std::string package_name = "com.test.app";
  const std::string app_name = "TestApp";
  const std::string activity_name = "TestActivity";
  const apps::PackageId package_id =
      apps::PackageId(apps::PackageType::kArc, package_name);
  const std::string app_id =
      ArcAppListPrefs::GetAppId(package_name, activity_name);

  // Skip check for official API key.
  app_service_proxy()->PromiseAppService()->SetSkipApiKeyCheckForTesting(true);

  // Test:
  // 1) Start the installation.
  // Expect 2 updates: Promise app registration, then Almanac response update.
  // Note: As the Almanac response is not mocked, the promise icon will fallback
  // to using a placeholder image.
  ExpectNumUpdates(/*num_updates=*/2);
  app_instance()->SendInstallationStarted(package_name);
  WaitForPromiseAppUpdates();

  // Confirm that the promise icon gets generated with the correct label.
  ash::AppListItem* launcher_item = GetAppListItem(package_id.ToString());
  ASSERT_TRUE(launcher_item);
  EXPECT_EQ(launcher_item->name(), "Waiting…");
  EXPECT_EQ(launcher_item->progress(), 0);
  // Close ARC connection.
  arc_app_list_pref()->app_connection_holder()->CloseInstance(app_instance());

  // Confirm that the promise icon is deleted when ARC connection is closed.
  launcher_item = GetAppListItem(package_id.ToString());
  ASSERT_FALSE(launcher_item);
}

}  // namespace apps