blob: 5449a2a3402b9c92d91db8c8a989e2ec6118f381 [file] [log] [blame]
// Copyright 2022 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/remote_apps/remote_apps_manager.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/quick_app_access_model.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/accelerators.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/test/app_list_test_api.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/barrier_closure.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/gtest_tags.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.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/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/remote_apps/id_generator.h"
#include "chrome/browser/ash/remote_apps/remote_apps_manager_factory.h"
#include "chrome/browser/ash/remote_apps/remote_apps_model.h"
#include "chrome/browser/ash/remote_apps/remote_apps_types.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/components/remote_apps/mojom/remote_apps.mojom.h"
#include "components/account_id/account_id.h"
#include "components/policy/proto/chrome_device_policy.pb.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/sync/protocol/app_list_specifics.pb.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 "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/user_manager/user_type.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/layout.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
namespace ash {
namespace {
constexpr char kId1[] = "id1";
constexpr char kId2[] = "id2";
constexpr char kId3[] = "id3";
constexpr char kId4[] = "id4";
constexpr char kMissingId[] = "missing_id";
constexpr char kExtensionId1[] = "extension_id1";
constexpr char kExtensionId2[] = "extension_id2";
class AppUpdateWaiter : public apps::AppRegistryCache::Observer {
public:
static base::RepeatingCallback<bool(const apps::AppUpdate&)> IconChanged() {
return base::BindRepeating([](const apps::AppUpdate& update) {
return !update.StateIsNull() && update.IconKeyChanged();
});
}
AppUpdateWaiter(
Profile* profile,
const std::string& id,
base::RepeatingCallback<bool(const apps::AppUpdate&)> condition =
base::BindRepeating([](const apps::AppUpdate& update) {
return true;
}))
: id_(id), condition_(condition) {
app_registry_cache_ = &apps::AppServiceProxyFactory::GetForProfile(profile)
->AppRegistryCache();
app_registry_cache_observation_.Observe(app_registry_cache_.get());
}
void Wait() {
if (!condition_met_) {
base::RunLoop run_loop;
callback_ = run_loop.QuitClosure();
run_loop.Run();
}
// Allow updates to propagate to other observers.
base::RunLoop().RunUntilIdle();
}
// apps::AppRegistryCache::Observer:
void OnAppUpdate(const apps::AppUpdate& update) override {
if (condition_met_ || update.AppId() != id_ || !condition_.Run(update))
return;
app_registry_cache_observation_.Reset();
condition_met_ = true;
if (callback_)
std::move(callback_).Run();
}
// apps::AppRegistryCache::Observer:
void OnAppRegistryCacheWillBeDestroyed(
apps::AppRegistryCache* cache) override {
app_registry_cache_observation_.Reset();
}
private:
std::string id_;
raw_ptr<apps::AppRegistryCache, ExperimentalAsh> app_registry_cache_ =
nullptr;
base::OnceClosure callback_;
base::RepeatingCallback<bool(const apps::AppUpdate&)> condition_;
bool condition_met_ = false;
base::ScopedObservation<apps::AppRegistryCache,
apps::AppRegistryCache::Observer>
app_registry_cache_observation_{this};
};
class MockImageDownloader : public RemoteAppsManager::ImageDownloader {
public:
MOCK_METHOD(void,
Download,
(const GURL& url,
base::OnceCallback<void(const gfx::ImageSkia&)> callback),
(override));
};
gfx::ImageSkia CreateTestIcon(int size, SkColor color) {
SkBitmap bitmap;
bitmap.allocN32Pixels(size, size);
bitmap.eraseColor(color);
gfx::ImageSkia image_skia;
const std::vector<ui::ResourceScaleFactor>& scale_factors =
ui::GetSupportedResourceScaleFactors();
for (auto& scale : scale_factors) {
image_skia.AddRepresentation(
gfx::ImageSkiaRep(bitmap, ui::GetScaleForResourceScaleFactor(scale)));
}
return image_skia;
}
void CheckIconsEqual(const gfx::ImageSkia& expected,
const gfx::ImageSkia& actual) {
EXPECT_TRUE(
gfx::test::AreBitmapsEqual(expected.GetRepresentation(1.0f).GetBitmap(),
actual.GetRepresentation(1.0f).GetBitmap()));
}
class MockRemoteAppLaunchObserver
: public chromeos::remote_apps::mojom::RemoteAppLaunchObserver {
public:
MOCK_METHOD(void,
OnRemoteAppLaunched,
(const std::string&, const std::string&),
(override));
};
} // namespace
class RemoteAppsManagerBrowsertest
: public policy::DevicePolicyCrosBrowserTest {
public:
RemoteAppsManagerBrowsertest() {
// Quick App is used for the current implementation of app pinning.
scoped_feature_list_.InitAndEnableFeature(
features::kHomeButtonQuickAppAccess);
}
// DevicePolicyCrosBrowserTest:
void SetUp() override {
DevicePolicyCrosBrowserTest::SetUp();
app_list::AppListSyncableServiceFactory::SetUseInTesting(true);
}
// DevicePolicyCrosBrowserTest:
void SetUpCommandLine(base::CommandLine* command_line) override {
DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kLoginManager);
command_line->AppendSwitch(switches::kForceLoginManagerInTests);
command_line->AppendSwitch(switches::kOobeSkipPostLogin);
}
// DevicePolicyCrosBrowserTest:
void SetUpOnMainThread() override {
SetUpDeviceLocalAccountPolicy();
SessionStateWaiter(session_manager::SessionState::ACTIVE).Wait();
user_manager::User* user =
user_manager::UserManager::Get()->GetActiveUser();
profile_ = ProfileHelper::Get()->GetProfileByUser(user);
app_list_syncable_service_ =
app_list::AppListSyncableServiceFactory::GetForProfile(profile_);
app_list_model_updater_ = app_list_syncable_service_->GetModelUpdater();
manager_ = RemoteAppsManagerFactory::GetForProfile(profile_);
std::unique_ptr<FakeIdGenerator> id_generator =
std::make_unique<FakeIdGenerator>(
std::vector<std::string>{kId1, kId2, kId3, kId4});
manager_->GetModelForTesting()->SetIdGeneratorForTesting(
std::move(id_generator));
std::unique_ptr<MockImageDownloader> image_downloader =
std::make_unique<MockImageDownloader>();
image_downloader_ = image_downloader.get();
manager_->SetImageDownloaderForTesting(std::move(image_downloader));
}
// TODO(b/239145899): Refactor to not use MGS setup any more.
void SetUpDeviceLocalAccountPolicy() {
enterprise_management::DeviceLocalAccountsProto* const
device_local_accounts =
device_policy()->payload().mutable_device_local_accounts();
enterprise_management::DeviceLocalAccountInfoProto* const account =
device_local_accounts->add_account();
account->set_account_id("user@test");
account->set_type(enterprise_management::DeviceLocalAccountInfoProto::
ACCOUNT_TYPE_PUBLIC_SESSION);
device_local_accounts->set_auto_login_id("user@test");
device_local_accounts->set_auto_login_delay(0);
RefreshDevicePolicy();
}
void ExpectImageDownloaderDownload(const GURL& icon_url,
const gfx::ImageSkia& icon) {
EXPECT_CALL(*image_downloader_, Download(icon_url, testing::_))
.WillOnce(
[icon](const GURL& icon_url,
base::OnceCallback<void(const gfx::ImageSkia&)> callback) {
std::move(callback).Run(icon);
});
}
AppListItem* GetAppListItem(const std::string& id) {
return AppListModelProvider::Get()->model()->FindItem(id);
}
std::string AddApp(const std::string& source_id,
const std::string& name,
const std::string& folder_id,
const GURL& icon_url,
bool add_to_front) {
base::test::TestFuture<std::string, RemoteAppsError> future;
manager_->AddApp(source_id, name, folder_id, icon_url, add_to_front,
future.GetCallback<const std::string&, RemoteAppsError>());
// Ideally ASSERT_EQ should be used, but we are in a non-void function.
EXPECT_EQ(RemoteAppsError::kNone, future.Get<1>());
return future.Get<0>();
}
RemoteAppsError DeleteApp(const std::string& id) {
RemoteAppsError error = manager_->DeleteApp(id);
// Allow updates to propagate to AppList.
base::RunLoop().RunUntilIdle();
return error;
}
RemoteAppsError DeleteFolder(const std::string& id) {
RemoteAppsError error = manager_->DeleteFolder(id);
// Allow updates to propagate to AppList.
base::RunLoop().RunUntilIdle();
return error;
}
void AddAppAndWaitForIconChange(const std::string& source_id,
const std::string& app_id,
const std::string& name,
const std::string& folder_id,
const GURL& icon_url,
const gfx::ImageSkia& icon,
bool add_to_front) {
ExpectImageDownloaderDownload(icon_url, icon);
AppUpdateWaiter waiter(profile_, app_id, AppUpdateWaiter::IconChanged());
AddApp(source_id, name, folder_id, icon_url, add_to_front);
waiter.Wait();
}
void AddAppAssertError(const std::string& source_id,
RemoteAppsError error,
const std::string& name,
const std::string& folder_id,
const GURL& icon_url,
bool add_to_front) {
base::test::TestFuture<std::string, RemoteAppsError> future;
manager_->AddApp(source_id, name, folder_id, icon_url, add_to_front,
future.GetCallback<const std::string&, RemoteAppsError>());
ASSERT_EQ(error, future.Get<1>());
}
void ShowLauncherAppsGrid(bool wait_for_opening_animation) {
AppListClientImpl* client = AppListClientImpl::GetInstance();
EXPECT_FALSE(client->GetAppListWindow());
ash::AcceleratorController::Get()->PerformActionIfEnabled(
AcceleratorAction::kToggleAppList, {});
ash::AppListTestApi().WaitForBubbleWindow(wait_for_opening_animation);
EXPECT_TRUE(client->GetAppListWindow());
}
std::string LoadExtension(const base::FilePath& extension_path) {
extensions::ChromeTestExtensionLoader loader(profile_);
loader.set_location(extensions::mojom::ManifestLocation::kExternalPolicy);
loader.set_pack_extension(true);
// When |set_pack_extension_| is true, the |loader| first packs and then
// loads the extension. The packing step creates a _metadata folder which
// causes an install warning when loading.
loader.set_ignore_manifest_warnings(true);
return loader.LoadExtension(extension_path)->id();
}
// Returns a list of app ids following the ordinal increasing order.
std::vector<std::string> GetAppIdsInOrdinalOrder(
const std::vector<std::string>& ids) {
std::vector<std::string> copy_ids(ids);
std::sort(
copy_ids.begin(), copy_ids.end(),
[&](const std::string& id1, const std::string& id2) {
return app_list_model_updater_->FindItem(id1)->position().LessThan(
app_list_model_updater_->FindItem(id2)->position());
});
return copy_ids;
}
void OnReorderAnimationDone(base::OnceClosure closure,
bool aborted,
AppListGridAnimationStatus status) {
EXPECT_FALSE(aborted);
EXPECT_EQ(AppListGridAnimationStatus::kReorderFadeIn, status);
std::move(closure).Run();
}
const std::string& PinnedAppId() {
return AppListModelProvider::Get()
->quick_app_access_model()
->quick_app_id();
}
void ExpectNoAppIsPinned() {
// When no app is pinned, QuickAppAccessMode::quick_app_id() returns an
// empty string.
EXPECT_EQ(PinnedAppId(), "");
}
protected:
// Launch healthcare application on device (COM_HEALTH_CUJ1_TASK2_WF1).
void AddScreenplayTag() {
base::AddTagToTestResult("feature_id",
"screenplay-446812cc-07af-4094-bfb2-00150301ede3");
}
raw_ptr<app_list::AppListSyncableService, ExperimentalAsh>
app_list_syncable_service_;
raw_ptr<AppListModelUpdater, ExperimentalAsh> app_list_model_updater_;
ash::AppListTestApi app_list_test_api_;
raw_ptr<RemoteAppsManager, ExperimentalAsh> manager_ = nullptr;
raw_ptr<MockImageDownloader, ExperimentalAsh> image_downloader_ = nullptr;
raw_ptr<Profile, ExperimentalAsh> profile_ = nullptr;
EmbeddedPolicyTestServerMixin policy_test_server_mixin_{&mixin_host_};
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddApp) {
AddScreenplayTag();
// Show launcher UI so that app icons are loaded.
ShowLauncherAppsGrid(/*wait_for_opening_animation=*/false);
std::string name = "name";
GURL icon_url("icon_url");
gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED);
// App has id kId1.
AddAppAndWaitForIconChange(kExtensionId1, kId1, name, std::string(), icon_url,
icon, /*add_to_front=*/false);
ash::AppListItem* item = GetAppListItem(kId1);
EXPECT_FALSE(item->is_folder());
EXPECT_EQ(name, item->name());
EXPECT_TRUE(item->GetMetadata()->is_ephemeral);
base::test::TestFuture<apps::IconValuePtr> future;
auto iv = std::make_unique<apps::IconValue>();
iv->icon_type = apps::IconType::kStandard;
iv->uncompressed = icon;
apps::ApplyIconEffects(
profile_, /*app_id=*/absl::nullopt, apps::IconEffects::kCrOsStandardIcon,
/*size_hint_in_dip=*/64, std::move(iv), future.GetCallback());
// App's icon is the downloaded icon.
CheckIconsEqual(future.Get()->uncompressed, item->GetDefaultIcon());
}
// Adds an app with an empty icon URL and checks if the app gets assigned the
// default placeholder icon.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppPlaceholderIcon) {
// Show launcher UI so that app icons are loaded.
ShowLauncherAppsGrid(/*wait_for_opening_animation=*/true);
const std::string name = "name";
// App has id kId1. The downloader returns an empty image that is replaced by
// a placeholder icon.
AddAppAndWaitForIconChange(kExtensionId1, kId1, name, std::string(), GURL(),
gfx::ImageSkia(), /*add_to_front=*/false);
ash::AppListItem* item = GetAppListItem(kId1);
EXPECT_FALSE(item->is_folder());
EXPECT_EQ(name, item->name());
base::test::TestFuture<apps::IconValuePtr> future;
auto iv = std::make_unique<apps::IconValue>();
iv->icon_type = apps::IconType::kStandard;
iv->uncompressed =
manager_->GetPlaceholderIcon(kId1, /*size_hint_in_dip=*/64);
iv->is_placeholder_icon = true;
apps::ApplyIconEffects(
profile_, /*app_id=*/absl::nullopt, apps::IconEffects::kCrOsStandardIcon,
/*size_hint_in_dip=*/64, std::move(iv), future.GetCallback());
// App's icon is placeholder.
// TODO(https://crbug.com/1345682): add a pixel diff test for this scenario.
CheckIconsEqual(future.Get()->uncompressed,
app_list_test_api_.GetTopLevelItemViewFromId(kId1)
->icon_image_for_test());
CheckIconsEqual(future.Get()->uncompressed, item->GetDefaultIcon());
// App list color sorting should still work for placeholder icons.
ui::test::EventGenerator event_generator(ash::Shell::GetPrimaryRootWindow());
ash::AppListTestApi::ReorderAnimationEndState actual_state;
app_list_test_api_.ReorderByMouseClickAtToplevelAppsGridMenu(
ash::AppListSortOrder::kColor,
ash::AppListTestApi::MenuType::kAppListNonFolderItemMenu,
&event_generator,
/*target_state=*/
ash::AppListTestApi::ReorderAnimationEndState::kCompleted, &actual_state);
EXPECT_EQ(ash::AppListTestApi::ReorderAnimationEndState::kCompleted,
actual_state);
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppError) {
std::string name = "name";
GURL icon_url("icon_url");
gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED);
AddAppAssertError(kExtensionId1, RemoteAppsError::kFolderIdDoesNotExist, name,
kMissingId, icon_url, /*add_to_front=*/false);
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAppErrorNotReady) {
std::string name = "name";
GURL icon_url("icon_url");
gfx::ImageSkia icon = CreateTestIcon(32, SK_ColorRED);
manager_->SetIsInitializedForTesting(false);
AddAppAssertError(kExtensionId1, RemoteAppsError::kNotReady, name,
std::string(), icon_url,
/*add_to_front=*/false);
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteApp) {
// App has id kId1.
AddAppAndWaitForIconChange(kExtensionId1, kId1, "name", std::string(),
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
RemoteAppsError error = DeleteApp(kId1);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(RemoteAppsError::kNone, error);
EXPECT_FALSE(GetAppListItem(kId1));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteAppError) {
EXPECT_EQ(RemoteAppsError::kAppIdDoesNotExist, DeleteApp(kMissingId));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddAndDeleteFolder) {
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// Empty folder has no AppListItem.
EXPECT_FALSE(GetAppListItem(kId1));
EXPECT_EQ(RemoteAppsError::kNone, DeleteFolder(kId1));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, DeleteFolderError) {
EXPECT_EQ(RemoteAppsError::kFolderIdDoesNotExist, DeleteFolder(kMissingId));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddFolderAndApp) {
AddScreenplayTag();
std::string folder_name = "folder_name";
// Folder has id kId1.
manager_->AddFolder(folder_name, /*add_to_front=*/false);
// Empty folder has no item.
EXPECT_FALSE(GetAppListItem(kId1));
// App has id kId2.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", kId1,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
// Folder item was created.
ash::AppListItem* folder_item = GetAppListItem(kId1);
EXPECT_TRUE(folder_item->is_folder());
EXPECT_TRUE(folder_item->GetMetadata()->is_ephemeral);
EXPECT_EQ(folder_name, folder_item->name());
EXPECT_EQ(1u, folder_item->ChildItemCount());
EXPECT_TRUE(folder_item->FindChildItem(kId2));
ash::AppListItem* item = GetAppListItem(kId2);
EXPECT_EQ(kId1, item->folder_id());
EXPECT_TRUE(item->GetMetadata()->is_ephemeral);
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest,
AddFolderWithMultipleApps) {
AddScreenplayTag();
// Folder has id kId1.
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// App has id kId2.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", kId1,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
// App has id kId3.
AddAppAndWaitForIconChange(kExtensionId1, kId3, "name2", kId1,
GURL("icon_url2"),
CreateTestIcon(32, SK_ColorBLUE),
/*add_to_front=*/false);
ash::AppListItem* folder_item = GetAppListItem(kId1);
EXPECT_EQ(2u, folder_item->ChildItemCount());
EXPECT_TRUE(folder_item->FindChildItem(kId2));
EXPECT_TRUE(folder_item->FindChildItem(kId3));
DeleteApp(kId2);
folder_item = GetAppListItem(kId1);
EXPECT_EQ(1u, folder_item->ChildItemCount());
EXPECT_FALSE(folder_item->FindChildItem(kId2));
DeleteApp(kId3);
// Empty folder is removed.
EXPECT_FALSE(GetAppListItem(kId1));
// App has id kId4.
AddAppAndWaitForIconChange(kExtensionId1, kId4, "name3", kId1,
GURL("icon_url3"),
CreateTestIcon(32, SK_ColorGREEN),
/*add_to_front=*/false);
// Folder is re-created.
folder_item = GetAppListItem(kId1);
EXPECT_EQ(1u, folder_item->ChildItemCount());
EXPECT_TRUE(folder_item->FindChildItem(kId4));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest,
DeleteFolderWithMultipleApps) {
// Folder has id kId1.
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// App has id kId2.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", kId1,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
// App has id kId3.
AddAppAndWaitForIconChange(kExtensionId1, kId3, "name2", kId1,
GURL("icon_url2"),
CreateTestIcon(32, SK_ColorBLUE),
/*add_to_front=*/false);
DeleteFolder(kId1);
// Folder is removed.
EXPECT_FALSE(GetAppListItem(kId1));
// Apps are moved to top-level.
ash::AppListItem* item1 = GetAppListItem(kId2);
EXPECT_EQ(std::string(), item1->folder_id());
ash::AppListItem* item2 = GetAppListItem(kId3);
EXPECT_EQ(std::string(), item2->folder_id());
}
// Verifies that folders are not removed after user moves all but single item
// from them.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest,
DontRemoveSingleItemFolders) {
// Folder has id kId1.
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// App has id kId2.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", kId1,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
// App has id kId3.
AddAppAndWaitForIconChange(kExtensionId1, kId3, "name2", kId1,
GURL("icon_url2"),
CreateTestIcon(32, SK_ColorBLUE),
/*add_to_front=*/false);
ash::AppListItem* folder_item = GetAppListItem(kId1);
EXPECT_EQ(2u, folder_item->ChildItemCount());
EXPECT_TRUE(folder_item->FindChildItem(kId2));
EXPECT_TRUE(folder_item->FindChildItem(kId3));
// Move kId2 item to root app list.
ash::AppListItem* item1 = GetAppListItem(kId2);
ASSERT_TRUE(item1);
ash::AppListModelProvider::Get()->model()->MoveItemToRootAt(
item1, folder_item->position().CreateBefore());
ASSERT_EQ(folder_item, GetAppListItem(kId1));
EXPECT_EQ(1u, folder_item->ChildItemCount());
EXPECT_FALSE(folder_item->FindChildItem(kId2));
EXPECT_TRUE(folder_item->FindChildItem(kId3));
}
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, AddToFront) {
AddScreenplayTag();
// Folder has id kId1.
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// App has id kId2.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", std::string(),
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
EXPECT_FALSE(manager_->ShouldAddToFront(kId1));
EXPECT_FALSE(manager_->ShouldAddToFront(kId2));
// Folder has id kId3.
manager_->AddFolder("folder_name2", /*add_to_front=*/true);
// App has id kId4.
AddAppAndWaitForIconChange(kExtensionId1, kId4, "name2", kId3,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/true);
EXPECT_TRUE(manager_->ShouldAddToFront(kId3));
// |add_to_front| disabled since app has a parent folder.
EXPECT_FALSE(manager_->ShouldAddToFront(kId4));
}
// Test that app launched events are only dispatched to the extension which
// added the app, and the all events are dispatched to the Lacros observer.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, OnAppLaunched) {
AddScreenplayTag();
base::test::TestFuture<std::string>
on_remote_app_launched_with_app_id1_future;
base::test::TestFuture<std::string>
on_remote_app_launched_with_app_id2_future;
base::test::TestFuture<std::string>
on_remote_app_launched_with_app_id1_to_proxy_future;
base::test::TestFuture<std::string>
on_remote_app_launched_with_app_id2_to_proxy_future;
testing::StrictMock<MockRemoteAppLaunchObserver> mockObserver1;
EXPECT_CALL(mockObserver1, OnRemoteAppLaunched(kId1, kExtensionId1))
.WillOnce([&on_remote_app_launched_with_app_id1_future](
const std::string& app_id, const std::string& source_id) {
on_remote_app_launched_with_app_id1_future.SetValue(app_id);
});
testing::StrictMock<MockRemoteAppLaunchObserver> mockObserver2;
EXPECT_CALL(mockObserver2, OnRemoteAppLaunched(kId2, kExtensionId2))
.WillOnce([&on_remote_app_launched_with_app_id2_future](
const std::string& app_id, const std::string& source_id) {
on_remote_app_launched_with_app_id2_future.SetValue(app_id);
});
mojo::Remote<chromeos::remote_apps::mojom::RemoteApps> remote1;
mojo::Remote<chromeos::remote_apps::mojom::RemoteApps> remote2;
mojo::Receiver<chromeos::remote_apps::mojom::RemoteAppLaunchObserver>
observer1{&mockObserver1};
mojo::Receiver<chromeos::remote_apps::mojom::RemoteAppLaunchObserver>
observer2{&mockObserver2};
manager_->BindRemoteAppsAndAppLaunchObserver(
kExtensionId1, remote1.BindNewPipeAndPassReceiver(),
observer1.BindNewPipeAndPassRemote());
manager_->BindRemoteAppsAndAppLaunchObserver(
kExtensionId2, remote2.BindNewPipeAndPassReceiver(),
observer2.BindNewPipeAndPassRemote());
testing::StrictMock<MockRemoteAppLaunchObserver> mockObserver3;
mojo::Remote<chromeos::remote_apps::mojom::RemoteApps> remote3;
mojo::Receiver<chromeos::remote_apps::mojom::RemoteAppLaunchObserver>
proxyObserver{&mockObserver3};
manager_->BindRemoteAppsAndAppLaunchObserverForLacros(
remote3.BindNewPipeAndPassReceiver(),
proxyObserver.BindNewPipeAndPassRemote());
EXPECT_CALL(mockObserver3, OnRemoteAppLaunched(kId1, kExtensionId1))
.WillOnce([&on_remote_app_launched_with_app_id1_to_proxy_future](
const std::string& app_id, const std::string& source_id) {
on_remote_app_launched_with_app_id1_to_proxy_future.SetValue(app_id);
});
// App has id kId1, added by kExtensionId1.
AddAppAndWaitForIconChange(kExtensionId1, kId1, "name", std::string(),
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
// App has id kId2, added by kExtensionId2.
AddAppAndWaitForIconChange(kExtensionId2, kId2, "name", std::string(),
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
manager_->LaunchApp(kId1);
ASSERT_EQ(kId1, on_remote_app_launched_with_app_id1_future.Get());
ASSERT_EQ(kId1, on_remote_app_launched_with_app_id1_to_proxy_future.Get());
ASSERT_FALSE(on_remote_app_launched_with_app_id2_future.IsReady());
EXPECT_CALL(mockObserver3, OnRemoteAppLaunched(kId2, kExtensionId2))
.WillOnce([&on_remote_app_launched_with_app_id2_to_proxy_future](
const std::string& app_id, const std::string& source_id) {
on_remote_app_launched_with_app_id2_to_proxy_future.SetValue(app_id);
});
manager_->LaunchApp(kId2);
ASSERT_EQ(kId2, on_remote_app_launched_with_app_id2_future.Get());
ASSERT_EQ(kId2, on_remote_app_launched_with_app_id2_to_proxy_future.Get());
}
// Remote app list items are not supposed to be synced. This test verifies that
// the added remote app items are marked as ephemeral and are not synced to
// local storage or uploaded to sync data.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, RemoteAppsNotSynced) {
std::unique_ptr<syncer::FakeSyncChangeProcessor> sync_processor =
std::make_unique<syncer::FakeSyncChangeProcessor>();
app_list_syncable_service_->MergeDataAndStartSyncing(
syncer::APP_LIST, {},
std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(
sync_processor.get()));
content::RunAllTasksUntilIdle();
// App has id kId1.
AddAppAndWaitForIconChange(kExtensionId1, kId1, "name", std::string(),
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
ash::AppListItem* item = GetAppListItem(kId1);
EXPECT_FALSE(item->is_folder());
EXPECT_TRUE(item->GetMetadata()->is_ephemeral);
// Remote app sync item 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(kId1);
EXPECT_FALSE(dict_item);
// Remote app sync item 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, kId1);
}
}
// Remote folder list items are not supposed to be synced. This test verifies
// that the added remote folder items are marked as ephemeral and are not synced
// to local storage or uploaded to sync data.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, RemoteFoldersNotSynced) {
std::unique_ptr<syncer::FakeSyncChangeProcessor> sync_processor =
std::make_unique<syncer::FakeSyncChangeProcessor>();
app_list_model_updater_->SetActive(true);
app_list_syncable_service_->MergeDataAndStartSyncing(
syncer::APP_LIST, {},
std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(
sync_processor.get()));
content::RunAllTasksUntilIdle();
// Folder has id kId1.
manager_->AddFolder("folder_name", /*add_to_front=*/false);
// App has id kId2. Parent is kId1.
AddAppAndWaitForIconChange(kExtensionId1, kId2, "name", kId1,
GURL("icon_url"), CreateTestIcon(32, SK_ColorRED),
/*add_to_front=*/false);
ash::AppListItem* item = GetAppListItem(kId1);
EXPECT_TRUE(item->is_folder());
EXPECT_TRUE(item->GetMetadata()->is_ephemeral);
// Remote folder sync item 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(kId1);
EXPECT_FALSE(dict_item);
// Remote folder sync item 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, kId1);
}
}
// Tests that the kAlphabeticalEphemeralAppFirst sort order moves the remote
// apps and folders to the front of the launcher, before all native items.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest,
SortLauncherWithRemoteAppsFirst) {
AddScreenplayTag();
// Show launcher UI so that app icons are loaded.
ShowLauncherAppsGrid(/*wait_for_opening_animation=*/true);
base::FilePath test_dir_path;
base::PathService::Get(chrome::DIR_TEST_DATA, &test_dir_path);
test_dir_path = test_dir_path.AppendASCII("extensions");
// Adds 2 remote apps.
// Make one app name lower case to test case insensitive.
std::string remote_app1_id =
AddApp(kExtensionId1, "test app 5", std::string(), GURL(),
/*add_to_front=*/true);
std::string remote_app2_id =
AddApp(kExtensionId1, "Test App 7", std::string(), GURL(),
/*add_to_front=*/true);
// Adds remote folder with one remote app inside.
std::string remote_folder_id =
manager_->AddFolder("Test App 6 Folder", /*add_to_front=*/true);
AddApp(kExtensionId1, "Test App 8", remote_folder_id, GURL(),
/*add_to_front=*/true);
// Adds 3 native apps.
std::string app1_id =
LoadExtension(test_dir_path.AppendASCII("app1")); // Test App 1
ASSERT_FALSE(app1_id.empty());
std::string app2_id =
LoadExtension(test_dir_path.AppendASCII("app2")); // Test App 2
ASSERT_FALSE(app2_id.empty());
std::string app3_id =
LoadExtension(test_dir_path.AppendASCII("app4")); // Test App 4
ASSERT_FALSE(app3_id.empty());
// Moves 2 native apps to a native folder.
const std::string native_folder_id =
app_list_test_api_.CreateFolderWithApps({app2_id, app3_id});
ash::AppListItem* folder_item = GetAppListItem(native_folder_id);
auto folder_metadata = folder_item->CloneMetadata();
folder_metadata->name = "Test App 2 Folder";
folder_item->SetMetadata(std::move(folder_metadata));
// Current order: `Test App 2 Folder` (native), `Test App 1` (native),
// `Test App 6 Folder` (remote), `Test App 7` (remote), `test app 5` (remote).
const std::vector<std::string> ids({app1_id, native_folder_id, remote_app1_id,
remote_app2_id, remote_folder_id});
EXPECT_EQ(
GetAppIdsInOrdinalOrder(ids),
std::vector<std::string>({native_folder_id, app1_id, remote_folder_id,
remote_app2_id, remote_app1_id}));
base::RunLoop run_loop;
app_list_test_api_.AddReorderAnimationCallback(
base::BindRepeating(&RemoteAppsManagerBrowsertest::OnReorderAnimationDone,
base::Unretained(this), run_loop.QuitClosure()));
manager_->SortLauncherWithRemoteAppsFirst();
run_loop.Run();
// Sorted order: `test app 5` (remote), `Test App 6 Folder` (remote),
// `Test App 7` (remote), `Test App 1` (native), `Test App 2 Folder` (native).
EXPECT_EQ(
GetAppIdsInOrdinalOrder(ids),
std::vector<std::string>({remote_app1_id, remote_folder_id,
remote_app2_id, app1_id, native_folder_id}));
}
// Tests that a single remote app can be pinned to the shelf and then unpinned.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, PinAndUnpinSingleApp) {
// Add a remote app.
std::string remote_app1_id =
AddApp(kExtensionId1, "test app 5", std::string(), GURL(),
/*add_to_front=*/true);
std::vector<std::string> app_ids_to_pin{remote_app1_id};
RemoteAppsError error1 = manager_->SetPinnedApps(app_ids_to_pin);
EXPECT_EQ(error1, RemoteAppsError::kNone);
EXPECT_EQ(PinnedAppId(), remote_app1_id);
// Empty list indicates that any currently pinned apps should be unpinned.
RemoteAppsError error2 = manager_->SetPinnedApps({});
EXPECT_EQ(error2, RemoteAppsError::kNone);
ExpectNoAppIsPinned();
}
// Pinning of multiple apps is not yet supported, but API allows it in case we
// will implement this in the future. Test that current implementation doesn't
// pin anything when asked to pin multiple apps.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest,
PinningMultipleAppsNotSupported) {
// Show launcher UI so that app icons are loaded.
ShowLauncherAppsGrid(/*wait_for_opening_animation=*/true);
// Adds2 remote apps.
std::string remote_app1_id =
AddApp(kExtensionId1, "test app 5", std::string(), GURL(),
/*add_to_front=*/true);
std::string remote_app2_id =
AddApp(kExtensionId1, "Test App 7", std::string(), GURL(),
/*add_to_front=*/true);
std::vector<std::string> app_ids_to_pin{remote_app1_id, remote_app2_id};
RemoteAppsError error = manager_->SetPinnedApps(app_ids_to_pin);
EXPECT_EQ(error, RemoteAppsError::kPinningMultipleAppsNotSupported);
ExpectNoAppIsPinned();
}
// Tests that nothing is pinned if we try to use an invalid app id.
IN_PROC_BROWSER_TEST_F(RemoteAppsManagerBrowsertest, PinInvalidApp) {
// No apps are added so there is nothing to pin.
std::vector<std::string> app_ids_to_pin{"invalid id"};
RemoteAppsError error = manager_->SetPinnedApps(app_ids_to_pin);
EXPECT_EQ(error, RemoteAppsError::kFailedToPinAnApp);
ExpectNoAppIsPinned();
}
} // namespace ash