| // Copyright 2017 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_list_syncable_service.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "ash/app_list/model/app_list_item.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/app_list_config.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/test/to_vector.h" |
| #include "build/build_config.h" |
| #include "cc/base/math_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/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_test_util.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/app_list/reorder/app_list_reorder_core.h" |
| #include "chrome/browser/ash/app_list/test/app_list_syncable_service_test_base.h" |
| #include "chrome/browser/ui/settings_window_manager_chromeos.h" |
| #include "chrome/browser/web_applications/test/web_app_install_test_utils.h" |
| #include "chrome/browser/web_applications/web_app_id_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/app_constants/constants.h" |
| #include "components/crx_file/id_util.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 "extensions/common/constants.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using crx_file::id_util::GenerateId; |
| using testing::ElementsAre; |
| using ItemTestApi = ChromeAppListItem::TestApi; |
| |
| namespace app_list { |
| |
| namespace { |
| |
| const char kOsSettingsUrl[] = "chrome://os-settings/"; |
| |
| // These constants are defined as functions so their values can be derived via |
| // function calls. The constant naming scheme is kept to maintain readability. |
| const std::string kInvalidOrdinalsId() { |
| return GenerateId("invalid_ordinals"); |
| } |
| const std::string kEmptyItemNameId() { |
| return GenerateId("empty_item_name"); |
| } |
| const std::string kEmptyItemNameUnsetId() { |
| return GenerateId("empty_item_name_unset"); |
| } |
| const std::string kEmptyParentId() { |
| return GenerateId("empty_parent_id"); |
| } |
| const std::string kEmptyParentUnsetId() { |
| return GenerateId("empty_parent_id_unset"); |
| } |
| const std::string kEmptyOrdinalsId() { |
| return GenerateId("empty_ordinals"); |
| } |
| const std::string kEmptyOrdinalsUnsetId() { |
| return GenerateId("empty_ordinals_unset"); |
| } |
| const std::string kDupeItemId() { |
| return GenerateId("dupe_item_id"); |
| } |
| const std::string kParentId() { |
| return GenerateId("parent_id"); |
| } |
| const std::string kEmptyPromisePackageId() { |
| return GenerateId("empty_package_id"); |
| } |
| const std::string kEmptyPromisePackageUnsetId() { |
| return GenerateId("unset_package_id"); |
| } |
| |
| syncer::SyncDataList CreateBadAppRemoteData(const std::string& id) { |
| syncer::SyncDataList sync_list; |
| // Invalid item_ordinal and item_pin_ordinal. |
| sync_list.push_back(CreateAppRemoteData( |
| id == kDefault ? kInvalidOrdinalsId() : id, "item_name", kParentId(), |
| "$$invalid_ordinal$$", "$$invalid_ordinal$$")); |
| // Empty item name. |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyItemNameId() : id, "", |
| kParentId(), "ordinal", "pinordinal")); |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyItemNameUnsetId() : id, kUnset, |
| kParentId(), "ordinal", "pinordinal")); |
| // Empty parent ID. |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyParentId() : id, "item_name", |
| "", "ordinal", "pinordinal")); |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyParentUnsetId() : id, |
| "item_name", kUnset, "ordinal", "pinordinal")); |
| // Empty item_ordinal and item_pin_ordinal. |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyOrdinalsId() : id, "item_name", |
| kParentId(), "", "")); |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyOrdinalsUnsetId() : id, |
| "item_name", kParentId(), kUnset, kUnset)); |
| // Duplicate item_id. |
| sync_list.push_back(CreateAppRemoteData(id == kDefault ? kDupeItemId() : id, |
| "item_name", kParentId(), "ordinal", |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(id == kDefault ? kDupeItemId() : id, |
| "item_name_dupe", kParentId(), |
| "ordinal", "pinordinal")); |
| // Empty item_id. |
| sync_list.push_back(CreateAppRemoteData("", "item_name", kParentId(), |
| "ordinal", "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kUnset, "item_name", kParentId(), |
| "ordinal", "pinordinal")); |
| // Empty promise_package_id. |
| sync_list.push_back( |
| CreateAppRemoteData(id == kDefault ? kEmptyPromisePackageId() : id, |
| "item_name", kParentId(), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| /*is_user_pinned=*/false, /*promise_package_id=*/"")); |
| sync_list.push_back(CreateAppRemoteData( |
| id == kDefault ? kEmptyPromisePackageUnsetId() : id, "item_name", |
| kParentId(), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| /*is_user_pinned=*/false, /*promise_package_id=*/kUnset)); |
| |
| // All fields empty. |
| sync_list.push_back(CreateAppRemoteData( |
| "", "", "", "", "", sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| std::nullopt, "")); |
| sync_list.push_back( |
| CreateAppRemoteData(kUnset, kUnset, kUnset, kUnset, kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| std::nullopt, kUnset)); |
| |
| return sync_list; |
| } |
| |
| bool AreAllAppAtributesEqualInSync( |
| const AppListSyncableService::SyncItem* item1, |
| const AppListSyncableService::SyncItem* item2) { |
| return item1->parent_id == item2->parent_id && |
| item1->item_ordinal.EqualsOrBothInvalid(item2->item_ordinal) && |
| item1->item_pin_ordinal.EqualsOrBothInvalid(item2->item_pin_ordinal); |
| } |
| |
| bool AreAllAppAtributesNotEqualInSync( |
| const AppListSyncableService::SyncItem* item1, |
| const AppListSyncableService::SyncItem* item2) { |
| return item1->parent_id != item2->parent_id && |
| !item1->item_ordinal.EqualsOrBothInvalid(item2->item_ordinal) && |
| !item1->item_pin_ordinal.EqualsOrBothInvalid(item2->item_pin_ordinal); |
| } |
| |
| bool AreAllAppAtributesEqualInAppList(const ChromeAppListItem* item1, |
| const ChromeAppListItem* item2) { |
| // Note, there is no pin position in app list. |
| return item1->folder_id() == item2->folder_id() && |
| item1->position().EqualsOrBothInvalid(item2->position()); |
| } |
| |
| bool AreAllAppAtributesNotEqualInAppList(const ChromeAppListItem* item1, |
| const ChromeAppListItem* item2) { |
| // Note, there is no pin position in app list. |
| return item1->folder_id() != item2->folder_id() && |
| !item1->position().EqualsOrBothInvalid(item2->position()); |
| } |
| |
| std::string GetLastPositionString() { |
| static syncer::StringOrdinal last_position; |
| if (!last_position.IsValid()) |
| last_position = syncer::StringOrdinal::CreateInitialOrdinal(); |
| else |
| last_position = last_position.CreateAfter(); |
| return last_position.ToDebugString(); |
| } |
| |
| } // namespace |
| |
| // The class that verifies app list syncable service features. Use a fake app |
| // list model updater during testing. |
| class AppListSyncableServiceTest : public test::AppListSyncableServiceTestBase { |
| public: |
| AppListSyncableServiceTest() { |
| feature_list_.InitAndEnableFeature( |
| ash::features::kRemoveStalePolicyPinnedAppsFromShelf); |
| } |
| AppListSyncableServiceTest(const AppListSyncableServiceTest&) = delete; |
| AppListSyncableServiceTest& operator=(const AppListSyncableServiceTest&) = |
| delete; |
| ~AppListSyncableServiceTest() override = default; |
| |
| // test::AppListSyncableServiceTestBase: |
| void SetUp() override { |
| AppListSyncableServiceTestBase::SetUp(); |
| model_updater_test_api_ = |
| std::make_unique<AppListModelUpdater::TestApi>(GetModelUpdater()); |
| } |
| |
| void TearDown() override { app_list_syncable_service_.reset(); } |
| |
| AppListModelUpdater::TestApi* model_updater_test_api() { |
| return model_updater_test_api_.get(); |
| } |
| |
| // Returns the app list order stored as preference. |
| ash::AppListSortOrder GetSortOrderFromPrefs() { |
| return static_cast<ash::AppListSortOrder>( |
| profile()->GetPrefs()->GetInteger(prefs::kAppListPreferredOrder)); |
| } |
| |
| ash::AppListItem* FindItemForApp(extensions::Extension* app) { |
| return GetModelUpdater()->model_for_test()->FindItem(app->id()); |
| } |
| |
| // A hacky way to change an item's name. |
| void ChangeItemName(const std::string& id, const std::string& new_name) { |
| const_cast<AppListSyncableService::SyncItem*>( |
| app_list_syncable_service()->GetSyncItem(id)) |
| ->item_name = new_name; |
| app_list_syncable_service()->GetModelUpdater()->SetItemName(id, new_name); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| std::unique_ptr<AppListModelUpdater::TestApi> model_updater_test_api_; |
| }; |
| |
| TEST_F(AppListSyncableServiceTest, OEMFolderForConflictingPos) { |
| // Create a "web store" app. |
| const std::string web_store_app_id(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> store = |
| MakeApp("webstore", web_store_app_id, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(store.get()); |
| |
| // Create some app. Note its id should be greater than web store app id in |
| // order to move app in case of conflicting pos after web store app. |
| const std::string test_app_1_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp(kSomeAppName, test_app_1_id, |
| extensions ::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(test_app_1.get()); |
| |
| const std::string test_app_2_id = CreateNextAppId(test_app_1_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp(kSomeAppName, test_app_2_id, |
| extensions ::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(test_app_2.get()); |
| |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* web_store_item = model_updater->FindItem(web_store_app_id); |
| ASSERT_TRUE(web_store_item); |
| ChromeAppListItem* test_app_1_item = model_updater->FindItem(test_app_1_id); |
| ASSERT_TRUE(test_app_1_item); |
| |
| ChromeAppListItem* test_app_2_item = model_updater->FindItem(test_app_2_id); |
| ASSERT_TRUE(test_app_2_item); |
| // Simulate position conflict. |
| model_updater_test_api()->SetItemPosition(web_store_item->id(), |
| test_app_1_item->position()); |
| |
| // Position second test app after the webstore and the first test app. |
| model_updater_test_api()->SetItemPosition( |
| test_app_2_item->id(), test_app_1_item->position().CreateAfter()); |
| |
| // Install an OEM app. It must be placed by default after web store app but in |
| // case of app of the same position should be shifted next. |
| const std::string oem_app_id = CreateNextAppId(test_app_2_id); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| InstallExtension(oem_app.get()); |
| |
| // OEM item is not top level element. |
| ChromeAppListItem* oem_app_item = model_updater->FindItem(oem_app_id); |
| EXPECT_NE(nullptr, oem_app_item); |
| EXPECT_EQ(oem_app_item->folder_id(), ash::kOemFolderId); |
| |
| // But OEM folder is. |
| ChromeAppListItem* oem_folder = model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_NE(nullptr, oem_folder); |
| EXPECT_EQ(oem_folder->folder_id(), ""); |
| |
| EXPECT_TRUE(oem_folder->position().GreaterThan(web_store_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(test_app_1_item->position())); |
| EXPECT_TRUE(oem_folder->position().LessThan(test_app_2_item->position())); |
| |
| // Receiving initial sync data does not change the OEM folder position. |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, syncer::SyncDataList(), |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| EXPECT_TRUE(oem_folder->position().GreaterThan(web_store_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(test_app_1_item->position())); |
| EXPECT_TRUE(oem_folder->position().LessThan(test_app_2_item->position())); |
| } |
| |
| // Verifies that OEM folder is positioned at the end of the list if initial sync |
| // data that contains non-default apps is received after an OEM data is |
| // received. |
| TEST_F(AppListSyncableServiceTest, |
| OEMFolderPositionUpdatedAfterInitialSyncWithNonDefaultApps) { |
| // Create a "web store" app. |
| const std::string web_store_app_id(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> store = |
| MakeApp("webstore", web_store_app_id, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(store.get()); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* web_store_item = model_updater->FindItem(web_store_app_id); |
| |
| const std::string test_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> test_app = |
| MakeApp(kSomeAppName, test_app_id, |
| extensions ::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(test_app.get()); |
| ChromeAppListItem* test_app_item = model_updater->FindItem(test_app_id); |
| model_updater_test_api()->SetItemPosition( |
| test_app_item->id(), web_store_item->position().CreateAfter()); |
| |
| // Install an OEM app. It must be placed by default after web store app but in |
| // case of app of the same position should be shifted next. |
| const std::string oem_app_id = CreateNextAppId(test_app_id); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| InstallExtension(oem_app.get()); |
| |
| syncer::StringOrdinal sync_item_ordinal = |
| test_app_item->position().CreateAfter(); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| "non-oem-app", "Non OEM app", std::string() /* parent_id */, |
| sync_item_ordinal.ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ChromeAppListItem* oem_folder = model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_NE(nullptr, oem_folder); |
| EXPECT_EQ(oem_folder->folder_id(), ""); |
| |
| EXPECT_TRUE(oem_folder->position().GreaterThan(web_store_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(test_app_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(sync_item_ordinal)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| OEMFolderPositionedAtEndIfNonDefaultAppsSynced) { |
| const std::string web_store_app_id(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> store = |
| MakeApp("webstore", web_store_app_id, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(store.get()); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* web_store_item = model_updater->FindItem(web_store_app_id); |
| |
| const std::string test_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> test_app = |
| MakeApp(kSomeAppName, test_app_id, |
| extensions ::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(test_app.get()); |
| ChromeAppListItem* test_app_item = model_updater->FindItem(test_app_id); |
| model_updater_test_api()->SetItemPosition( |
| test_app_item->id(), web_store_item->position().CreateAfter()); |
| |
| syncer::StringOrdinal sync_item_ordinal = |
| test_app_item->position().CreateAfter(); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| "non-oem-app", "Non OEM app", std::string() /* parent_id */, |
| sync_item_ordinal.ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Install an OEM app. It must be placed by default after web store app but in |
| // case of app of the same position should be shifted next. |
| const std::string oem_app_id = CreateNextAppId(test_app_id); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| InstallExtension(oem_app.get()); |
| |
| ChromeAppListItem* oem_folder = model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_NE(nullptr, oem_folder); |
| EXPECT_EQ(oem_folder->folder_id(), ""); |
| |
| EXPECT_TRUE(oem_folder->position().GreaterThan(web_store_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(test_app_item->position())); |
| EXPECT_TRUE(oem_folder->position().GreaterThan(sync_item_ordinal)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| OEMFolderPositionedAfterWebstoreIfOnlyDefaultAppsSynced) { |
| const std::string web_store_app_id(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> store = |
| MakeApp("webstore", web_store_app_id, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(store.get()); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* web_store_item = model_updater->FindItem(web_store_app_id); |
| |
| const std::string test_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> test_app = |
| MakeApp(kSomeAppName, test_app_id, |
| extensions ::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(test_app.get()); |
| ChromeAppListItem* test_app_item = model_updater->FindItem(test_app_id); |
| model_updater_test_api()->SetItemPosition( |
| test_app_item->id(), web_store_item->position().CreateAfter()); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, syncer::SyncDataList(), |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| const std::string oem_app_id = CreateNextAppId(test_app_id); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| InstallExtension(oem_app.get()); |
| |
| ChromeAppListItem* oem_folder = model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_NE(nullptr, oem_folder); |
| EXPECT_EQ(oem_folder->folder_id(), ""); |
| |
| EXPECT_TRUE(oem_folder->position().GreaterThan(web_store_item->position())); |
| EXPECT_TRUE(oem_folder->position().LessThan(test_app_item->position())); |
| } |
| |
| // Verifies that OEM item preserves parent and doesn't change parent in case |
| // sync change says this. |
| TEST_F(AppListSyncableServiceTest, OEMItemIgnoreSyncParent) { |
| const std::string oem_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| InstallExtension(oem_app.get()); |
| |
| // OEM item is not top level element. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* oem_app_item = model_updater->FindItem(oem_app_id); |
| ASSERT_TRUE(oem_app_item); |
| EXPECT_EQ(ash::kOemFolderId, oem_app_item->folder_id()); |
| |
| // Send sync that OEM app is top-level item. |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| oem_app_id, kOemAppName, std::string() /* parent_id */, |
| oem_app_item->position().ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Parent folder is not changed. |
| EXPECT_EQ(ash::kOemFolderId, oem_app_item->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, OEMAppParentNotOverridenInSync) { |
| const std::string oem_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| const std::string oem_app_parent_in_sync = "nonoemfolder"; |
| |
| // Send sync where the OEM app is parented by another folder. |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| oem_app_parent_in_sync, "Non OEM folder", std::string() /* parent_id */, |
| syncer::StringOrdinal("nonoemfolderposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal*/, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData( |
| oem_app_id, kOemAppName, oem_app_parent_in_sync, |
| syncer::StringOrdinal("appposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| // Add an extra app to the folder to avoid invalid single item folder. |
| sync_list.push_back(CreateAppRemoteData( |
| "non-oem-app", "Non OEM app", oem_app_parent_in_sync, |
| syncer::StringOrdinal("nonoemappposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| sync_list.push_back(CreateAppRemoteData( |
| ash::kOemFolderId, "OEM", std::string() /*parent_id*/, |
| syncer::StringOrdinal("oemposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal*/, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| InstallExtension(oem_app.get()); |
| |
| // The OEM app should be parented by the OEM folder locally. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* oem_app_item = model_updater->FindItem(oem_app_id); |
| ASSERT_TRUE(oem_app_item); |
| EXPECT_EQ(ash::kOemFolderId, oem_app_item->folder_id()); |
| |
| ChromeAppListItem* oem_folder_item = |
| model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_TRUE(oem_folder_item); |
| EXPECT_EQ(oem_folder_item->position(), syncer::StringOrdinal("oemposition")); |
| |
| // Verify that the OEM parent has no changed in sync. |
| const AppListSyncableService::SyncItem* app_sync_item = |
| GetSyncItem(oem_app_id); |
| ASSERT_TRUE(app_sync_item); |
| EXPECT_EQ(oem_app_parent_in_sync, app_sync_item->parent_id); |
| |
| // Verify that the non OEM folder is not removed from sync, even though it's |
| // not been created locally. |
| EXPECT_FALSE(model_updater->FindItem(oem_app_parent_in_sync)); |
| EXPECT_TRUE(GetSyncItem(oem_app_parent_in_sync)); |
| } |
| |
| // Verifies that OEM folder position respects the OEM folder position in sync. |
| TEST_F(AppListSyncableServiceTest, OEMFolderPositionSync) { |
| const std::string oem_app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> oem_app = MakeApp( |
| kOemAppName, oem_app_id, extensions::Extension::WAS_INSTALLED_BY_OEM); |
| |
| // Send sync with an OEM folder item. |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| oem_app_id, kOemAppName, std::string() /* parent_id */, |
| syncer::StringOrdinal("appposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| sync_list.push_back(CreateAppRemoteData( |
| ash::kOemFolderId, "OEM", std::string() /*parent_id*/, |
| syncer::StringOrdinal("oemposition").ToInternalValue(), |
| std::string() /* item_pin_ordinal*/, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| InstallExtension(oem_app.get()); |
| |
| // OEM app should locally be parented by the OEM folder. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* oem_app_item = model_updater->FindItem(oem_app_id); |
| ASSERT_TRUE(oem_app_item); |
| EXPECT_EQ(ash::kOemFolderId, oem_app_item->folder_id()); |
| |
| ChromeAppListItem* oem_folder_item = |
| model_updater->FindItem(ash::kOemFolderId); |
| ASSERT_TRUE(oem_folder_item); |
| // The OEM folder folder should be set to the value set by sync. |
| EXPECT_EQ(oem_folder_item->position(), syncer::StringOrdinal("oemposition")); |
| } |
| |
| // Verifies that non-OEM item is not moved to OEM folder by sync. |
| TEST_F(AppListSyncableServiceTest, NonOEMItemIgnoreSyncToOEMFolder) { |
| const std::string app_id = CreateNextAppId(extensions::kWebStoreAppId); |
| scoped_refptr<extensions::Extension> app = MakeApp( |
| kSomeAppName, app_id, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app.get()); |
| |
| ChromeAppListItem* app_item = GetModelUpdater()->FindItem(app_id); |
| ASSERT_TRUE(app_item); |
| // It is in the top list. |
| EXPECT_EQ(std::string(), app_item->folder_id()); |
| |
| // Send sync that this app is in OEM folder. |
| syncer::SyncDataList sync_list; |
| sync_list.push_back( |
| CreateAppRemoteData(app_id, kSomeAppName, ash::kOemFolderId, |
| app_item->position().ToInternalValue(), |
| std::string() /* item_pin_ordinal */)); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Parent folder is not changed. |
| EXPECT_EQ(std::string(), app_item->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, InitialMerge) { |
| const std::string kItemId1 = GenerateId("item_id1"); |
| const std::string kItemId2 = GenerateId("item_id2"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kItemId1, "item_name1", GenerateId("parent_id1"), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt, |
| "promise_package_id1")); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemId2, "item_name2", GenerateId("parent_id2"), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt, |
| "promise_package_id2")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| EXPECT_EQ("item_name1", GetSyncItem(kItemId1)->item_name); |
| EXPECT_EQ(GenerateId("parent_id1"), GetSyncItem(kItemId1)->parent_id); |
| EXPECT_EQ("ordinal", GetSyncItem(kItemId1)->item_ordinal.ToDebugString()); |
| EXPECT_EQ("pinordinal", |
| GetSyncItem(kItemId1)->item_pin_ordinal.ToDebugString()); |
| EXPECT_EQ("promise_package_id1", GetSyncItem(kItemId1)->promise_package_id); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| EXPECT_EQ("item_name2", GetSyncItem(kItemId2)->item_name); |
| EXPECT_EQ(GenerateId("parent_id2"), GetSyncItem(kItemId2)->parent_id); |
| EXPECT_EQ("ordinal", GetSyncItem(kItemId2)->item_ordinal.ToDebugString()); |
| EXPECT_EQ("pinordinal", |
| GetSyncItem(kItemId2)->item_pin_ordinal.ToDebugString()); |
| EXPECT_EQ("promise_package_id2", GetSyncItem(kItemId2)->promise_package_id); |
| } |
| |
| class AppListInternalAppSyncableServiceTest |
| : public AppListSyncableServiceTest { |
| public: |
| AppListInternalAppSyncableServiceTest() { |
| chrome::SettingsWindowManager::ForceDeprecatedSettingsWindowForTesting(); |
| } |
| |
| void SetUp() override { |
| AppListSyncableServiceTest::SetUp(); |
| web_app::test::InstallDummyWebApp(testing_profile(), kOsSettingsUrl, |
| GURL(kOsSettingsUrl)); |
| } |
| |
| ~AppListInternalAppSyncableServiceTest() override = default; |
| }; |
| |
| TEST_F(AppListSyncableServiceTest, InitialMerge_BadData) { |
| const syncer::SyncDataList sync_list = CreateBadAppRemoteData(kDefault); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Invalid item_ordinal and item_pin_ordinal. |
| // Invalid item_ordinal is fixed up. |
| ASSERT_TRUE(GetSyncItem(kInvalidOrdinalsId())); |
| EXPECT_EQ("n", |
| GetSyncItem(kInvalidOrdinalsId())->item_ordinal.ToDebugString()); |
| EXPECT_EQ( |
| "INVALID[$$invalid_ordinal$$]", |
| GetSyncItem(kInvalidOrdinalsId())->item_pin_ordinal.ToDebugString()); |
| |
| // Empty item name. |
| ASSERT_TRUE(GetSyncItem(kEmptyItemNameId())); |
| EXPECT_EQ("", GetSyncItem(kEmptyItemNameId())->item_name); |
| EXPECT_TRUE(GetSyncItem(kEmptyItemNameUnsetId())); |
| EXPECT_EQ("", GetSyncItem(kEmptyItemNameUnsetId())->item_name); |
| |
| // Empty parent ID. |
| ASSERT_TRUE(GetSyncItem(kEmptyParentId())); |
| EXPECT_EQ("", GetSyncItem(kEmptyParentId())->parent_id); |
| EXPECT_TRUE(GetSyncItem(kEmptyParentUnsetId())); |
| EXPECT_EQ("", GetSyncItem(kEmptyParentUnsetId())->parent_id); |
| |
| // Empty item_ordinal and item_pin_ordinal. |
| // Empty item_ordinal is fixed up. |
| ASSERT_TRUE(GetSyncItem(kEmptyOrdinalsId())); |
| EXPECT_EQ("n", GetSyncItem(kEmptyOrdinalsId())->item_ordinal.ToDebugString()); |
| EXPECT_EQ("INVALID[]", |
| GetSyncItem(kEmptyOrdinalsId())->item_pin_ordinal.ToDebugString()); |
| ASSERT_TRUE(GetSyncItem(kEmptyOrdinalsUnsetId())); |
| EXPECT_EQ("n", |
| GetSyncItem(kEmptyOrdinalsUnsetId())->item_ordinal.ToDebugString()); |
| EXPECT_EQ( |
| "INVALID[]", |
| GetSyncItem(kEmptyOrdinalsUnsetId())->item_pin_ordinal.ToDebugString()); |
| |
| // Duplicate item_id overrides previous. |
| ASSERT_TRUE(GetSyncItem(kDupeItemId())); |
| EXPECT_EQ("item_name_dupe", GetSyncItem(kDupeItemId())->item_name); |
| |
| // Empty promise_package_id. |
| ASSERT_TRUE(GetSyncItem(kEmptyPromisePackageId())); |
| EXPECT_TRUE( |
| GetSyncItem(kEmptyPromisePackageId())->promise_package_id.empty()); |
| EXPECT_TRUE(GetSyncItem(kEmptyPromisePackageUnsetId())); |
| EXPECT_TRUE( |
| GetSyncItem(kEmptyPromisePackageUnsetId())->promise_package_id.empty()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, InitialMergeAndUpdate) { |
| RemoveAllExistingItems(); |
| |
| const std::string kItemId1 = GenerateId("item_id1"); |
| const std::string kItemId2 = GenerateId("item_id2"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kItemId1, "item_name1", kParentId(), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, std::nullopt, |
| "promise_package_id1")); |
| sync_list.push_back(CreateAppRemoteData(kItemId2, "item_name2", kParentId(), |
| "ordinal", "pinordinal")); |
| |
| auto sync_processor = std::make_unique<syncer::FakeSyncChangeProcessor>(); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::SyncChangeProcessorWrapperForTest>( |
| sync_processor.get())); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| |
| syncer::SyncChangeList change_list; |
| change_list.push_back(syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_UPDATE, |
| CreateAppRemoteData(kItemId1, "item_name1x", GenerateId("parent_id1x"), |
| "ordinalx", "pinordinalx", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| /*is_user_pinned=*/true, "promise_package_id1x"))); |
| change_list.push_back(syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_UPDATE, |
| CreateAppRemoteData(kItemId2, "item_name2x", GenerateId("parent_id2x"), |
| "ordinalx", "pinordinalx", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| /*is_user_pinned=*/false, "promise_package_id2"))); |
| |
| app_list_syncable_service()->ProcessSyncChanges(base::Location(), |
| change_list); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| EXPECT_EQ("item_name1x", GetSyncItem(kItemId1)->item_name); |
| EXPECT_EQ(GenerateId("parent_id1x"), GetSyncItem(kItemId1)->parent_id); |
| EXPECT_EQ("ordinalx", GetSyncItem(kItemId1)->item_ordinal.ToDebugString()); |
| EXPECT_EQ("pinordinalx", |
| GetSyncItem(kItemId1)->item_pin_ordinal.ToDebugString()); |
| EXPECT_TRUE(GetSyncItem(kItemId1)->is_user_pinned.has_value()); |
| EXPECT_TRUE(*GetSyncItem(kItemId1)->is_user_pinned); |
| EXPECT_FALSE(GetSyncItem(kItemId1)->promise_package_id.empty()); |
| EXPECT_EQ("promise_package_id1x", GetSyncItem(kItemId1)->promise_package_id); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| EXPECT_EQ("item_name2x", GetSyncItem(kItemId2)->item_name); |
| EXPECT_EQ(GenerateId("parent_id2x"), GetSyncItem(kItemId2)->parent_id); |
| EXPECT_EQ("ordinalx", GetSyncItem(kItemId2)->item_ordinal.ToDebugString()); |
| EXPECT_EQ("pinordinalx", |
| GetSyncItem(kItemId2)->item_pin_ordinal.ToDebugString()); |
| EXPECT_TRUE(GetSyncItem(kItemId2)->is_user_pinned.has_value()); |
| EXPECT_FALSE(*GetSyncItem(kItemId2)->is_user_pinned); |
| EXPECT_FALSE(GetSyncItem(kItemId2)->promise_package_id.empty()); |
| EXPECT_EQ("promise_package_id2", GetSyncItem(kItemId2)->promise_package_id); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, InitialMergeAndUpdate_BadData) { |
| const std::string kItemId = GenerateId("item_id"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kItemId, "item_name", kParentId(), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_APP, |
| /*is_user_pinned=*/false, "promise_package_id")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId)); |
| |
| // Validate items with bad data are processed without crashing. |
| app_list_syncable_service()->ProcessSyncChanges( |
| FROM_HERE, base::test::ToVector( |
| CreateBadAppRemoteData(kItemId), [](const auto& update) { |
| return syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_UPDATE, |
| update); |
| })); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, HandlesItemWithNonExistantFolderId) { |
| const std::string kFolderItemId = "folder_item_id"; |
| const std::string kItemId = GenerateId("item_id"); |
| const std::string kNonInstalledItemId = GenerateId("not_installed_item_id"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData(kItemId, "item_name", kFolderItemId, |
| "ordinal", "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kNonInstalledItemId, "item_name", |
| kFolderItemId, "ordinal2", |
| "pinordinal")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| scoped_refptr<extensions::Extension> app = MakeApp( |
| "Test app", kItemId, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app.get()); |
| |
| // Verify that the item install does not crash - the app is moved to the root |
| // apps grid. |
| ASSERT_TRUE(GetSyncItem(kItemId)); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* installed_item = model_updater->FindItem(kItemId); |
| ASSERT_TRUE(installed_item); |
| EXPECT_EQ("", installed_item->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, AddingFolderChildItemWithInvalidPosition) { |
| const std::string kFolderItemId = "folder_item_id"; |
| const std::string kItemId = GenerateId("item_id"); |
| const std::string kNonInstalledItemId = GenerateId("not_installed_item_id"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderItemId, "folder_item_name", "", "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kItemId, "item_name", kFolderItemId, |
| "$$invalid_ordinal$$", "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kNonInstalledItemId, "item_name", |
| kFolderItemId, "ordinal", |
| "pinordinal")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| scoped_refptr<extensions::Extension> app = MakeApp( |
| "Test app", kItemId, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app.get()); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId)); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* installed_item = model_updater->FindItem(kItemId); |
| ASSERT_TRUE(installed_item); |
| EXPECT_EQ(kFolderItemId, installed_item->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| AddingFolderChildItemWithSiblingWithInvalidPosition) { |
| const std::string kFolderItemId = "folder_item_id"; |
| const std::string kItemId1 = GenerateId("item_id_1"); |
| const std::string kItemId2 = GenerateId("item_id_2"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderItemId, "folder_item_name", "", "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kItemId1, "item1_name", kFolderItemId, |
| "ordinal1", "pinordinal1")); |
| sync_list.push_back(CreateAppRemoteData(kItemId2, "item2_name", kFolderItemId, |
| "$$invalid_ordinal$$", |
| "pinordinal2")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| scoped_refptr<extensions::Extension> app_2 = MakeApp( |
| "Test app", kItemId2, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_2.get()); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* item_2 = model_updater->FindItem(kItemId2); |
| ASSERT_TRUE(item_2); |
| EXPECT_EQ(kFolderItemId, item_2->folder_id()); |
| |
| scoped_refptr<extensions::Extension> app_1 = MakeApp( |
| "Test app", kItemId1, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_1.get()); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| ChromeAppListItem* item_1 = model_updater->FindItem(kItemId1); |
| ASSERT_TRUE(item_1); |
| EXPECT_EQ(kFolderItemId, item_1->folder_id()); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| item_2 = model_updater->FindItem(kItemId2); |
| ASSERT_TRUE(item_2); |
| EXPECT_EQ(kFolderItemId, item_2->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, SyncFolderMoveWithInvalidOrdinalInfo) { |
| const std::string kFolderItemId = "folder_item_id"; |
| const std::string kItemId1 = GenerateId("item_id_1"); |
| const std::string kItemId2 = GenerateId("item_id_2"); |
| |
| scoped_refptr<extensions::Extension> app_2 = MakeApp( |
| "Test app", kItemId2, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_2.get()); |
| |
| scoped_refptr<extensions::Extension> app_1 = MakeApp( |
| "Test app", kItemId1, extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_1.get()); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderItemId, "folder_item_name", "", "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kItemId1, "item1_name", kFolderItemId, |
| "ordinal1", "pinordinal1")); |
| sync_list.push_back(CreateAppRemoteData(kItemId2, "item2_name", kFolderItemId, |
| "$$invalid_ordinal$$", |
| "pinordinal2")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* item_1 = model_updater->FindItem(kItemId1); |
| ASSERT_TRUE(item_1); |
| EXPECT_EQ(kFolderItemId, item_1->folder_id()); |
| |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| ChromeAppListItem* item_2 = model_updater->FindItem(kItemId2); |
| ASSERT_TRUE(item_2); |
| EXPECT_EQ(kFolderItemId, item_2->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, PruneEmptySyncFolder) { |
| // Add a folder item and two items that are parented to the folder item. |
| const std::string kFolderItemId = GenerateId("folder_item_id"); |
| const std::string kItemId1 = GenerateId("item_id_1"); |
| const std::string kItemId2 = GenerateId("item_id_2"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderItemId, "folder_item_name", kParentId(), "ordinal", "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kItemId1, "item1_name", kFolderItemId, |
| "ordinal1", "pinordinal1")); |
| sync_list.push_back(CreateAppRemoteData(kItemId2, "item2_name", kFolderItemId, |
| "ordinal2", "pinordinal2")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kFolderItemId)); |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| |
| // Remove one of the child item, the folder still has one item in it. |
| app_list_syncable_service()->RemoveItem(kItemId1, /*is_uninstall=*/false); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kFolderItemId)); |
| ASSERT_FALSE(GetSyncItem(kItemId1)); |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| |
| // Remove the other child item, the empty folder should be removed as well. |
| app_list_syncable_service()->RemoveItem(kItemId2, /*is_uninstall=*/false); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_FALSE(GetSyncItem(kFolderItemId)); |
| ASSERT_FALSE(GetSyncItem(kItemId2)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| CleanUpSingleItemSyncFolderAfterInitialMerge) { |
| syncer::SyncDataList sync_list; |
| // Add a top level item. |
| const std::string kTopItem = GenerateId("top_item_id"); |
| sync_list.push_back(CreateAppRemoteData( |
| kTopItem, "top_item_name", "", GetLastPositionString(), "pinordinal")); |
| |
| // Add a single app folder item with only one child app item in it. |
| const std::string kFolderId1 = GenerateId("folder_id_1"); |
| const std::string kChildItemId1 = GenerateId("child_item_id_1"); |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId1, "folder_1_name", "", GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId1, "child_item_1_name", |
| kFolderId1, GetLastPositionString(), |
| "pinordinal")); |
| |
| // Add a regular folder with two app items in it. |
| const std::string kFolderId2 = GenerateId("folder_id_2"); |
| const std::string kChildItemId2 = GenerateId("child_item_id_2"); |
| const std::string kChildItemId3 = GenerateId("child_item_id_3"); |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId2, "folder_2_name", "", GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId2, "child_item_2_name", |
| kFolderId2, GetLastPositionString(), |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId3, "child_item_3_name", |
| kFolderId2, GetLastPositionString(), |
| "pinordinal")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kFolderId1)); |
| ASSERT_TRUE(GetSyncItem(kChildItemId1)); |
| EXPECT_EQ(kFolderId1, GetSyncItem(kChildItemId1)->parent_id); |
| EXPECT_TRUE( |
| GetSyncItem(kChildItemId1) |
| ->item_ordinal.GreaterThan(GetSyncItem(kTopItem)->item_ordinal)); |
| |
| // Sync items should be created for regular folder. |
| ASSERT_TRUE(GetSyncItem(kFolderId2)); |
| ASSERT_TRUE(GetSyncItem(kChildItemId2)); |
| EXPECT_EQ(kFolderId2, GetSyncItem(kChildItemId2)->parent_id); |
| ASSERT_TRUE(GetSyncItem(kChildItemId3)); |
| EXPECT_EQ(kFolderId2, GetSyncItem(kChildItemId3)->parent_id); |
| EXPECT_TRUE( |
| GetSyncItem(kChildItemId1) |
| ->item_ordinal.LessThan(GetSyncItem(kFolderId2)->item_ordinal)); |
| } |
| |
| // Simulates and verifies the fix of the single item folder issue of |
| // crbug.com/1082530. Here is the repro of the bug. |
| // When user signs in on a new device for the first time, a folder contains two |
| // app items, one is installed before another. After the first app is installed, |
| // user sees a single item folder with the first app. User moves the app out of |
| // the folder, the folder disappears from UI. Later, when the second app is |
| // installed, it will show up in a folder with only the second app in it. |
| // The fix will remove the second app from the folder after user removes the |
| // first app. Later, when the second app is installed, it will show at the top |
| // level. |
| TEST_F(AppListSyncableServiceTest, UpdateSyncItemRemoveLastItemFromFolder) { |
| // Create two apps associated with a folder for sync data. |
| const std::string kFolderId = GenerateId("folder_id"); |
| const std::string kChildItemId1 = extensions::kWebStoreAppId; |
| const std::string kChildItemId2 = CreateNextAppId(extensions::kWebStoreAppId); |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId, "folder_name", "", GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId1, "child_item_1_name", |
| kFolderId, GetLastPositionString(), |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId2, "child_item_2_name", |
| kFolderId, GetLastPositionString(), |
| "pinordinal")); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kFolderId)); |
| ASSERT_TRUE(GetSyncItem(kChildItemId1)); |
| ASSERT_TRUE(GetSyncItem(kChildItemId2)); |
| EXPECT_EQ(kFolderId, GetSyncItem(kChildItemId1)->parent_id); |
| EXPECT_EQ(kFolderId, GetSyncItem(kChildItemId2)->parent_id); |
| |
| // Install the first child app. |
| scoped_refptr<extensions::Extension> child_app_1 = |
| MakeApp("child_item_1_name", kChildItemId1, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(child_app_1.get()); |
| |
| // Verify the first child app is created in the model updater. |
| // The second app is not in the model updater since it is not installed yet. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* child_item_1 = model_updater->FindItem(kChildItemId1); |
| ASSERT_TRUE(child_item_1); |
| EXPECT_EQ(kFolderId, child_item_1->folder_id()); |
| ASSERT_FALSE(model_updater->FindItem(kChildItemId2)); |
| |
| // Move the child_item_1 out of the folder. |
| model_updater->SetItemFolderId(child_item_1->id(), ""); |
| |
| ASSERT_TRUE(GetSyncItem(kChildItemId1)); |
| EXPECT_EQ("", GetSyncItem(kChildItemId1)->parent_id); |
| ASSERT_TRUE(GetSyncItem(kChildItemId2)); |
| EXPECT_EQ(kFolderId, GetSyncItem(kChildItemId2)->parent_id); |
| EXPECT_EQ("", model_updater->FindItem(kChildItemId1)->folder_id()); |
| |
| // Install the second child app. |
| scoped_refptr<extensions::Extension> child_app_2 = |
| MakeApp("child_item_2_name", kChildItemId2, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(child_app_2.get()); |
| |
| ChromeAppListItem* child_item_2 = model_updater->FindItem(kChildItemId2); |
| ASSERT_TRUE(child_item_2); |
| EXPECT_EQ(kFolderId, child_item_2->folder_id()); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, PruneRedundantPageBreakItems) { |
| RemoveAllExistingItems(); |
| |
| // Populate item list with items and leading, trailing and duplicate "page |
| // break" items. |
| const std::string kPageBreakItemId1 = GenerateId("page_break_item_id1"); |
| const std::string kItemId1 = GenerateId("item_id1"); |
| const std::string kFolderItemId = GenerateId("folder_item_id"); |
| const std::string kPageBreakItemId2 = GenerateId("page_break_item_id2"); |
| const std::string kItemInFolderId = GenerateId("item_in_folder_id"); |
| const std::string kPageBreakItemId3 = GenerateId("page_break_item_id3"); |
| const std::string kPageBreakItemId4 = GenerateId("page_break_item_id4"); |
| const std::string kItemId2 = GenerateId("item_id2"); |
| const std::string kPageBreakItemId5 = GenerateId("page_break_item_id5"); |
| |
| syncer::SyncDataList sync_list; |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId1, "page_break_item_name", "" /* parent_id */, |
| "b" /* ordinal */, "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| sync_list.push_back(CreateAppRemoteData(kItemId1, "item_name", |
| "" /* parent_id */, "c" /* ordinal */, |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData(kFolderItemId, "folder_item_name", |
| "" /* parent_id */, "d" /* ordinal */, |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId2, "page_break_item_name", "" /* parent_id */, |
| "e" /* ordinal */, "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemInFolderId, "item_in_folder_name", kFolderItemId /* parent_id */, |
| "f" /* ordinal */, "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId3, "page_break_item_name", "" /* parent_id */, |
| "g" /* ordinal */, "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId4, "page_break_item_name", "" /* parent_id */, |
| "h" /* ordinal */, "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| sync_list.push_back(CreateAppRemoteData(kItemId2, "item_name", |
| "" /* parent_id */, "i" /* ordinal */, |
| "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId5, "page_break_item_name", "" /* parent_id */, |
| "j" /* ordinal */, "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId1)); |
| ASSERT_TRUE(GetSyncItem(kItemId1)); |
| ASSERT_TRUE(GetSyncItem(kFolderItemId)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId2)); |
| ASSERT_TRUE(GetSyncItem(kItemInFolderId)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId3)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId4)); |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId5)); |
| |
| // Remove a item, which triggers removing redundant "page break" items. |
| app_list_syncable_service()->RemoveItem(kItemId1, /*is_uninstall=*/false); |
| content::RunAllTasksUntilIdle(); |
| |
| ASSERT_FALSE(GetSyncItem(kPageBreakItemId1)); |
| ASSERT_FALSE(GetSyncItem(kItemId1)); |
| ASSERT_TRUE(GetSyncItem(kFolderItemId)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId2)); |
| ASSERT_TRUE(GetSyncItem(kItemInFolderId)); |
| ASSERT_FALSE(GetSyncItem(kPageBreakItemId3)); |
| ASSERT_FALSE(GetSyncItem(kPageBreakItemId4)); |
| ASSERT_TRUE(GetSyncItem(kItemId2)); |
| ASSERT_FALSE(GetSyncItem(kPageBreakItemId5)); |
| } |
| |
| // This test simulates the following overflow case. Assume the maximum items |
| // on each page is three. Both device 1 and device 2 have the identical apps, |
| // the apps layout looks like the following. |
| // page 1: A1, A2 [page break] |
| // page 2: B1, B2, B3 [page break] |
| // page 3: C1 [page break] |
| // On device 1, move A1 from page 1 to page 2 and insert between B1 and B2. |
| // After the move, the apps layout should look like the following |
| // page 1: A2 [page break] |
| // page 2: B1, A1, B2 [page break] |
| // page 3: B3, C1 [page break] |
| // Notice that B3 is overflowed from page 2 to page 3 and placed before C1. |
| // After the changes are synced to device 2, it should have the same app layout |
| // as shown on device 1. |
| // This test simulates that device 2 gets the sync changes from device 1, and |
| // applies the changes in model updater and the apps should have the same layout |
| // as the ones on the device 1. It verifies the fix for the repro issue |
| // described in http://crbug.com/938098#c15. |
| TEST_F(AppListSyncableServiceTest, PageBreakWithOverflowItem) { |
| RemoveAllExistingItems(); |
| |
| // Create 2 apps on the page 1. |
| syncer::SyncDataList sync_list; |
| const std::string kItemIdA1 = extensions::kWebStoreAppId; |
| const std::string kItemIdA2 = CreateNextAppId(extensions::kWebStoreAppId); |
| const std::string kPageBreakItemId1 = GenerateId("page_break_item_id1"); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdA1, "A1", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdA2, "A2", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId1, "page_break_item1_name", "" /* parent_id */, |
| GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| |
| // Create 3 apps on the page 2, which makes it a full page. |
| const std::string kItemIdB1 = CreateNextAppId(kItemIdA2); |
| const std::string kItemIdB2 = CreateNextAppId(kItemIdB1); |
| const std::string kItemIdB3 = CreateNextAppId(kItemIdB2); |
| const std::string kPageBreakItemId2 = GenerateId("page_break_item_id2"); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdB1, "B1", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdB2, "B2", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdB3, "B3", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId2, "page_break_item2_name", "" /* parent_id */, |
| GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| |
| // Create 1 app on page 3. |
| const std::string kItemIdC1 = CreateNextAppId(kItemIdB3); |
| const std::string kPageBreakItemId3 = GenerateId("page_break_item_id3"); |
| sync_list.push_back(CreateAppRemoteData( |
| kItemIdC1, "C1", "", GetLastPositionString(), "pinordinal")); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreakItemId3, "page_break_item3_name", "" /* parent_id */, |
| GetLastPositionString(), "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Verify the sync items on page 1. |
| ASSERT_TRUE(GetSyncItem(kItemIdA1)); |
| ASSERT_TRUE(GetSyncItem(kItemIdA2)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId1)); |
| |
| // Verify the sync items on page 2. |
| ASSERT_TRUE(GetSyncItem(kItemIdB1)); |
| ASSERT_TRUE(GetSyncItem(kItemIdB2)); |
| ASSERT_TRUE(GetSyncItem(kItemIdB3)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId2)); |
| |
| // Verify the sync items on page 3. |
| ASSERT_TRUE(GetSyncItem(kItemIdC1)); |
| ASSERT_TRUE(GetSyncItem(kPageBreakItemId3)); |
| |
| // Install all the apps. |
| scoped_refptr<extensions::Extension> app_A1 = |
| MakeApp("app_A1_name", kItemIdA1, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_A1.get()); |
| scoped_refptr<extensions::Extension> app_A2 = |
| MakeApp("app_A2_name", kItemIdA2, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_A2.get()); |
| scoped_refptr<extensions::Extension> app_B1 = |
| MakeApp("app_B1_name", kItemIdB1, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_B1.get()); |
| scoped_refptr<extensions::Extension> app_B2 = |
| MakeApp("app_B2_name", kItemIdB2, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_B2.get()); |
| scoped_refptr<extensions::Extension> app_B3 = |
| MakeApp("app_B3_name", kItemIdB3, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_B3.get()); |
| scoped_refptr<extensions::Extension> app_C1 = |
| MakeApp("app_C1_name", kItemIdC1, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(app_C1.get()); |
| |
| auto ordered_items = GetOrderedItemIdsFromModelUpdater(); |
| EXPECT_THAT(ordered_items, ElementsAre(kItemIdA1, kItemIdA2, kItemIdB1, |
| kItemIdB2, kItemIdB3, kItemIdC1)); |
| |
| // On device 1, move A1 from page 1 to page 2 and insert it between B1 and B2. |
| // Device 2 should get the following 3 sync changes from device 1: |
| // 1. Remove the previous page break after B3. |
| // 2. Add a new page break after B2. |
| // 3. Update A1 for position change to move it between B1 and B2. |
| syncer::SyncChangeList change_list; |
| // Sync change for removing the previous page break after B3. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| ChromeAppListItem* app_item_B1 = model_updater->FindItem(kItemIdB1); |
| ChromeAppListItem* app_item_B2 = model_updater->FindItem(kItemIdB2); |
| ChromeAppListItem* app_item_B3 = model_updater->FindItem(kItemIdB3); |
| change_list.push_back(syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_DELETE, |
| CreateAppRemoteData( |
| kPageBreakItemId2, "page_break_item2_name", "" /* parent_id */, |
| GetSyncItem(kPageBreakItemId2)->item_ordinal.ToDebugString(), |
| "pinordinal"))); |
| // Sync change for adding a new page break after B2. |
| const std::string kNewPageBreakItemId = GenerateId("new_page_break_item_id"); |
| change_list.push_back(syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_ADD, |
| CreateAppRemoteData( |
| kNewPageBreakItemId, "new_page_break_item_name", "" /* parent_id */, |
| app_item_B2->position() |
| .CreateBetween(app_item_B3->position()) |
| .ToDebugString(), |
| "pinordinal", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK))); |
| // Sync change for moving A1 between B1 and B2. |
| change_list.push_back(syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_UPDATE, |
| CreateAppRemoteData(kItemIdA1, "A1", "" /* parent_id */, |
| app_item_B1->position() |
| .CreateBetween(app_item_B2->position()) |
| .ToDebugString(), |
| "pinordinal"))); |
| |
| app_list_syncable_service()->ProcessSyncChanges(base::Location(), |
| change_list); |
| content::RunAllTasksUntilIdle(); |
| |
| // Verify the original page break item after B3 is removed. |
| EXPECT_FALSE(GetSyncItem(kPageBreakItemId2)); |
| EXPECT_FALSE(model_updater->FindItem(kPageBreakItemId2)); |
| |
| // Verify a new page break sync item is created. |
| EXPECT_TRUE(GetSyncItem(kNewPageBreakItemId)); |
| |
| auto ordered_items_after_sync = GetOrderedItemIdsFromModelUpdater(); |
| EXPECT_THAT(ordered_items_after_sync, |
| ElementsAre(kItemIdA2, kItemIdB1, kItemIdA1, kItemIdB2, kItemIdB3, |
| kItemIdC1)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, FirstAvailablePosition) { |
| RemoveAllExistingItems(); |
| |
| // Populate the first page with items and leave 1 empty slot at the end. |
| const int max_items_in_first_page = |
| ash::SharedAppListConfig::instance().GetMaxNumOfItemsPerPage(); |
| syncer::StringOrdinal last_app_position = |
| syncer::StringOrdinal::CreateInitialOrdinal(); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| for (int i = 0; i < max_items_in_first_page - 1; ++i) { |
| std::unique_ptr<ChromeAppListItem> item = |
| std::make_unique<ChromeAppListItem>( |
| profile_.get(), GenerateId("item_id" + base::NumberToString(i)), |
| model_updater); |
| ItemTestApi(item.get()).SetPosition(last_app_position); |
| model_updater->AddItem(std::move(item)); |
| if (i < max_items_in_first_page - 2) |
| last_app_position = last_app_position.CreateAfter(); |
| } |
| EXPECT_TRUE(last_app_position.CreateAfter().Equals( |
| model_updater->GetFirstAvailablePosition())); |
| |
| EXPECT_TRUE(last_app_position.CreateAfter().Equals( |
| model_updater->GetFirstAvailablePosition())); |
| |
| // Fill up the first page. |
| std::unique_ptr<ChromeAppListItem> app_item = |
| std::make_unique<ChromeAppListItem>( |
| profile_.get(), |
| GenerateId("item_id" + base::NumberToString(max_items_in_first_page)), |
| model_updater); |
| const syncer::StringOrdinal new_item_position = |
| last_app_position.CreateAfter(); |
| ItemTestApi(app_item.get()).SetPosition(new_item_position); |
| model_updater->AddItem(std::move(app_item)); |
| EXPECT_TRUE(new_item_position.CreateAfter().Equals( |
| model_updater->GetFirstAvailablePosition())); |
| } |
| |
| // Test that verifies app attributes are transferred to the existing app and to |
| // to the app which will be installed later. |
| TEST_F(AppListSyncableServiceTest, TransferItem) { |
| // Webstore app in this test is source app. |
| scoped_refptr<extensions::Extension> webstore = |
| MakeApp(kSomeAppName, extensions::kWebStoreAppId, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(webstore.get()); |
| |
| // Chrome is an existing app to transfer attributes to. |
| scoped_refptr<extensions::Extension> chrome = |
| MakeApp(kSomeAppName, app_constants::kChromeAppId, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| InstallExtension(chrome.get()); |
| |
| // Youtube is a future app to be installed. |
| scoped_refptr<extensions::Extension> youtube = |
| MakeApp(kSomeAppName, extension_misc::kYoutubeAppId, |
| extensions::Extension::WAS_INSTALLED_BY_DEFAULT); |
| |
| // Webstore and Chrome items should exist now in sync and in model but not |
| // Youtube. |
| const AppListSyncableService::SyncItem* webstore_sync_item = |
| GetSyncItem(extensions::kWebStoreAppId); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| const ChromeAppListItem* webstore_item = |
| model_updater->FindItem(extensions::kWebStoreAppId); |
| ASSERT_TRUE(webstore_item); |
| ASSERT_TRUE(webstore_sync_item); |
| |
| const AppListSyncableService::SyncItem* chrome_sync_item = |
| GetSyncItem(app_constants::kChromeAppId); |
| const ChromeAppListItem* chrome_item = |
| model_updater->FindItem(app_constants::kChromeAppId); |
| ASSERT_TRUE(chrome_item); |
| ASSERT_TRUE(chrome_sync_item); |
| |
| EXPECT_FALSE(GetSyncItem(extension_misc::kYoutubeAppId)); |
| EXPECT_FALSE(model_updater->FindItem(extension_misc::kYoutubeAppId)); |
| |
| // Modify Webstore app with non-default attributes. |
| model_updater->SetItemPosition(extensions::kWebStoreAppId, |
| syncer::StringOrdinal("position")); |
| model_updater->SetItemFolderId(extensions::kWebStoreAppId, "folderid"); |
| app_list_syncable_service()->SetPinPosition(extensions::kWebStoreAppId, |
| syncer::StringOrdinal("pin"), |
| /*pinned_by_policy=*/false); |
| |
| // Before transfer attributes are different in both, app item and in sync. |
| EXPECT_TRUE(AreAllAppAtributesNotEqualInAppList(webstore_item, chrome_item)); |
| EXPECT_TRUE( |
| AreAllAppAtributesNotEqualInSync(webstore_sync_item, chrome_sync_item)); |
| |
| // Perform attributes transfer to existing Chrome app. |
| EXPECT_TRUE(app_list_syncable_service()->TransferItemAttributes( |
| extensions::kWebStoreAppId, app_constants::kChromeAppId)); |
| // Perform attributes transfer to the future Youtube app. |
| EXPECT_TRUE(app_list_syncable_service()->TransferItemAttributes( |
| extensions::kWebStoreAppId, extension_misc::kYoutubeAppId)); |
| // No sync item is created due to transfer to the future app. |
| EXPECT_FALSE(GetSyncItem(extension_misc::kYoutubeAppId)); |
| // Attributes transfer from non-existing app fails. |
| EXPECT_FALSE(app_list_syncable_service()->TransferItemAttributes( |
| "NonExistingId", extension_misc::kYoutubeAppId)); |
| |
| // Now Chrome app attributes match Webstore app. |
| EXPECT_TRUE(AreAllAppAtributesEqualInAppList(webstore_item, chrome_item)); |
| EXPECT_TRUE( |
| AreAllAppAtributesEqualInSync(webstore_sync_item, chrome_sync_item)); |
| |
| // Install Youtube now. |
| InstallExtension(youtube.get()); |
| |
| const AppListSyncableService::SyncItem* youtube_sync_item = |
| GetSyncItem(extension_misc::kYoutubeAppId); |
| const ChromeAppListItem* youtube_item = |
| model_updater->FindItem(extension_misc::kYoutubeAppId); |
| ASSERT_TRUE(youtube_item); |
| ASSERT_TRUE(youtube_sync_item); |
| |
| EXPECT_TRUE(AreAllAppAtributesEqualInAppList(webstore_item, youtube_item)); |
| EXPECT_TRUE( |
| AreAllAppAtributesEqualInSync(webstore_sync_item, youtube_sync_item)); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, EphemeralAppsNotSynced) { |
| RemoveAllExistingItems(); |
| |
| auto 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(); |
| |
| const std::string ephemeral_app_id = |
| CreateNextAppId(extensions::kWebStoreAppId); |
| auto* model_updater = GetModelUpdater(); |
| auto* app_item = model_updater->FindItem(ephemeral_app_id); |
| EXPECT_FALSE(app_item); |
| EXPECT_FALSE(GetSyncItem(ephemeral_app_id)); |
| |
| std::unique_ptr<ChromeAppListItem> ephemeral_app_item = |
| std::make_unique<ChromeAppListItem>(profile_.get(), ephemeral_app_id, |
| model_updater); |
| ephemeral_app_item->SetIsEphemeral(true); |
| // Can't use InstallExtension() because it calls AppRegistryCache::OnApps() |
| // with the same app ID but type kChromeApp. |
| app_list_syncable_service()->AddItem(std::move(ephemeral_app_item)); |
| |
| app_item = model_updater->FindItem(ephemeral_app_id); |
| ASSERT_TRUE(app_item); |
| EXPECT_TRUE(app_item->is_ephemeral()); |
| |
| auto* sync_item = GetSyncItem(ephemeral_app_id); |
| ASSERT_TRUE(sync_item); |
| EXPECT_TRUE(sync_item->is_ephemeral); |
| |
| // Ephemeral sync items are not added to the local storage. |
| const base::Value::Dict& local_items = |
| profile_->GetPrefs()->GetDict(prefs::kAppListLocalState); |
| |
| const base::Value::Dict* dict_item = local_items.FindDict(ephemeral_app_id); |
| EXPECT_FALSE(dict_item); |
| |
| // Ephemeral sync items are 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, ephemeral_app_id); |
| } |
| } |
| |
| TEST_F(AppListSyncableServiceTest, EphemeralFoldersNotSynced) { |
| RemoveAllExistingItems(); |
| |
| std::unique_ptr<syncer::FakeSyncChangeProcessor> sync_processor = |
| std::make_unique<syncer::FakeSyncChangeProcessor>(); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, {}, |
| std::unique_ptr<syncer::SyncChangeProcessor>( |
| new syncer::SyncChangeProcessorWrapperForTest(sync_processor.get()))); |
| content::RunAllTasksUntilIdle(); |
| |
| const std::string ephemeral_folder_id = GenerateId("folder_id"); |
| auto* model_updater = GetModelUpdater(); |
| auto* folder_item = model_updater->FindItem(ephemeral_folder_id); |
| EXPECT_FALSE(folder_item); |
| EXPECT_FALSE(GetSyncItem(ephemeral_folder_id)); |
| |
| syncer::StringOrdinal position = |
| syncer::StringOrdinal::CreateInitialOrdinal(); |
| std::unique_ptr<ChromeAppListItem> ephemeral_folder_item = |
| std::make_unique<ChromeAppListItem>(profile_.get(), ephemeral_folder_id, |
| model_updater); |
| ephemeral_folder_item->SetChromeIsFolder(true); |
| ephemeral_folder_item->SetChromeName("Folder"); |
| ephemeral_folder_item->SetIsSystemFolder(true); |
| ephemeral_folder_item->SetIsEphemeral(true); |
| app_list_syncable_service()->AddItem(std::move(ephemeral_folder_item)); |
| |
| folder_item = model_updater->FindItem(ephemeral_folder_id); |
| ASSERT_TRUE(folder_item); |
| EXPECT_TRUE(folder_item->is_ephemeral()); |
| |
| auto* sync_item = GetSyncItem(ephemeral_folder_id); |
| ASSERT_TRUE(sync_item); |
| EXPECT_TRUE(sync_item->is_ephemeral); |
| |
| // Ephemeral sync items are not added to the local storage. |
| const base::Value::Dict& local_items = |
| profile_->GetPrefs()->GetDict(prefs::kAppListLocalState); |
| const base::Value::Dict* dict_item = |
| local_items.FindDict(ephemeral_folder_id); |
| EXPECT_FALSE(dict_item); |
| |
| // Ephemeral sync items are 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, ephemeral_folder_id); |
| } |
| } |
| |
| TEST_F(AppListSyncableServiceTest, SanitizePagesOnItemAdditionAndRemoval) { |
| RemoveAllExistingItems(); |
| |
| // Add enough items to fill up a legacy app list page. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i < 20; ++i) { |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| GetLastPositionString(), kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", |
| "Item 10", "Item 11", "Item 12", "Item 13", "Item 14", |
| "Item 15", "Item 16", "Item 17", "Item 18", "Item 19"}})); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| // Verify a page break was added to sync data. |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18"}, |
| {"Item 19"}})); |
| |
| // Installing another app will not create a new page - the last item from |
| // first page will instead be moved to the second page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", |
| "Item 8", "Item 9", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17"}, |
| {"Item 18", "Item 19"}})); |
| |
| // Remove an app from the first page and verify first app from second page is |
| // moved back to the first page. |
| RemoveExtension(initial_apps[2]->id()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18"}, |
| {"Item 19"}})); |
| |
| // Removing another extension from the first page removes the second page. |
| RemoveExtension(initial_apps[11]->id()); |
| |
| EXPECT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 12", "Item 13", "Item 14", |
| "Item 15", "Item 16", "Item 17", "Item 18", "Item 19"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, SanitizationKeepsUserAddedPageBreaks) { |
| RemoveAllExistingItems(); |
| |
| // Add enough items to have two pages with legacy max app list page size, |
| // and add a page break, which is treated as a user added page break after the |
| // first page. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i < 21; ++i) { |
| if (i == 20) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break", "page_break", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| GetLastPositionString(), kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", |
| "Item 10", "Item 11", "Item 12", "Item 13", "Item 14", |
| "Item 15", "Item 16", "Item 17", "Item 18", "Item 19"}, |
| {"Item 20"}})); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| // Verify a page break was added to sync data. |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18"}, |
| {"Item 19"}, |
| {"Item 20"}})); |
| |
| // Installing another app will not create a new page - the last item from |
| // first page will instead be moved to the second page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", |
| "Item 8", "Item 9", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17"}, |
| {"Item 18", "Item 19"}, |
| {"Item 20"}})); |
| |
| // Remove an app from the first page and verify first app from second page is |
| // moved back to the first page. |
| RemoveExtension(initial_apps[2]->id()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18"}, |
| {"Item 19"}, |
| {"Item 20"}})); |
| |
| // Removing another extension from the first page removes the second page. |
| RemoveExtension(initial_apps[11]->id()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 12", "Item 13", "Item 14", |
| "Item 15", "Item 16", "Item 17", "Item 18", "Item 19"}, |
| {"Item 20"}})); |
| |
| // Remove another app, and verify the app from the second page remains on the |
| // second page. |
| RemoveExtension(initial_apps[12]->id()); |
| |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", |
| "Item 10", "Item 13", "Item 14", "Item 15", "Item 16", |
| "Item 17", "Item 18", "Item 19"}, |
| {"Item 20"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, SanitizePageSizesWhenMovingApps) { |
| RemoveAllExistingItems(); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, {}, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Add enough items to have two pages with legacy max app list page size. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i < 21; ++i) { |
| last_item_id = CreateNextAppId(last_item_id); |
| initial_apps.push_back(MakeApp(base::StringPrintf("Item %d", i), |
| last_item_id, |
| extensions::Extension::NO_FLAGS)); |
| } |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", |
| "Item 15", "Item 14", "Item 13", "Item 12", "Item 11", |
| "Item 10", "Item 9", "Item 8", "Item 7", "Item 6", |
| "Item 5", "Item 4", "Item 3", "Item 2", "Item 1"}, |
| {"Item 0"}})); |
| |
| // Move the item from the second page to the first page, and verify the number |
| // of the pages remains the same (and the last item on the first page moves to |
| // the second page). |
| ash::AppListItem* item_5 = FindItemForApp(initial_apps[5].get()); |
| ASSERT_TRUE(item_5); |
| ash::AppListItem* item_6 = FindItemForApp(initial_apps[6].get()); |
| ASSERT_TRUE(item_6); |
| syncer::StringOrdinal target_position = |
| item_5->position().CreateBetween(item_6->position()); |
| |
| GetModelUpdater()->RequestPositionUpdate( |
| initial_apps[0]->id(), target_position, |
| ash::RequestPositionUpdateReason::kMoveItem); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", |
| "Item 15", "Item 14", "Item 13", "Item 12", "Item 11", |
| "Item 10", "Item 9", "Item 8", "Item 7", "Item 6", |
| "Item 0", "Item 5", "Item 4", "Item 3", "Item 2"}, |
| {"Item 1"}})); |
| |
| // Install another app, and verify this does not add an extra page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 1", "Item 20", "Item 19", "Item 18", "Item 17", |
| "Item 16", "Item 15", "Item 14", "Item 13", "Item 12", |
| "Item 11", "Item 10", "Item 9", "Item 8", "Item 7", |
| "Item 6", "Item 0", "Item 5", "Item 4", "Item 3"}, |
| {"Item 2", "Item 1"}})); |
| |
| // Move an app from the first page to the second page, and verify no extra |
| // pages are created. |
| ash::AppListItem* item_2 = FindItemForApp(initial_apps[2].get()); |
| ASSERT_TRUE(item_2); |
| ash::AppListItem* item_1 = FindItemForApp(initial_apps[1].get()); |
| ASSERT_TRUE(item_1); |
| target_position = item_2->position().CreateBetween(item_1->position()); |
| |
| GetModelUpdater()->RequestPositionUpdate( |
| initial_apps[10]->id(), target_position, |
| ash::RequestPositionUpdateReason::kMoveItem); |
| |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 1", "Item 20", "Item 19", "Item 18", "Item 17", |
| "Item 16", "Item 15", "Item 14", "Item 13", "Item 12", |
| "Item 11", "Item 9", "Item 8", "Item 7", "Item 6", |
| "Item 0", "Item 5", "Item 4", "Item 3", "Item 2"}, |
| {"Item 10", "Item 1"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| SanitizePageSizesWhenCreatingAndRemovingFolders) { |
| RemoveAllExistingItems(); |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, {}, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Add enough items to have two pages with legacy max app list page size. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i < 21; ++i) { |
| last_item_id = CreateNextAppId(last_item_id); |
| initial_apps.push_back(MakeApp(base::StringPrintf("Item %d", i), |
| last_item_id, |
| extensions::Extension::NO_FLAGS)); |
| } |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", |
| "Item 15", "Item 14", "Item 13", "Item 12", "Item 11", |
| "Item 10", "Item 9", "Item 8", "Item 7", "Item 6", |
| "Item 5", "Item 4", "Item 3", "Item 2", "Item 1"}, |
| {"Item 0"}})); |
| |
| // Merge 2 items on the first page, and verify the second page gets removed. |
| const std::string folder_id = GetModelUpdater()->model_for_test()->MergeItems( |
| initial_apps[5]->id(), initial_apps[6]->id()); |
| ASSERT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", |
| "Item 15", "Item 14", "Item 13", "Item 12", "Item 11", |
| "Item 10", "Item 9", "Item 8", "Item 7", "" /*unnamed folder*/, |
| "Item 4", "Item 3", "Item 2", "Item 1", "Item 0"}})); |
| |
| // Move an item from the created folder, and verify another page gets |
| // created. |
| ash::AppListItem* item_2 = FindItemForApp(initial_apps[2].get()); |
| ASSERT_TRUE(item_2); |
| ash::AppListItem* item_1 = FindItemForApp(initial_apps[1].get()); |
| ASSERT_TRUE(item_1); |
| syncer::StringOrdinal target_position = |
| item_2->position().CreateBetween(item_1->position()); |
| |
| ash::AppListItem* item_6 = FindItemForApp(initial_apps[6].get()); |
| ASSERT_TRUE(item_6); |
| |
| GetModelUpdater()->model_for_test()->MoveItemToRootAt(item_6, |
| target_position); |
| |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", |
| "Item 15", "Item 14", "Item 13", "Item 12", "Item 11", |
| "Item 10", "Item 9", "Item 8", "Item 7", "", |
| "Item 4", "Item 3", "Item 2", "Item 6", "Item 1"}, |
| {"Item 0"}})); |
| |
| // Move item 5 out of folder to the second page, and verify the item on the |
| // second page fills the empty space left by the folder removal. |
| // Note that when productivity launcher is enabled, single item folders are |
| // allowed, so Item 5 is expected to still be in the folder at this point. |
| ash::AppListItem* item_5 = FindItemForApp(initial_apps[5].get()); |
| ASSERT_TRUE(item_5); |
| |
| ash::AppListItem* item_0 = FindItemForApp(initial_apps[0].get()); |
| ASSERT_TRUE(item_0); |
| GetModelUpdater()->model_for_test()->MoveItemToRootAt( |
| item_5, item_0->position().CreateAfter()); |
| |
| EXPECT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 20", "Item 19", "Item 18", "Item 17", "Item 16", "Item 15", |
| "Item 14", "Item 13", "Item 12", "Item 11", "Item 10", "Item 9", |
| "Item 8", "Item 7", "", "Item 4", "Item 3", "Item 2", |
| "Item 6", "Item 1", "Item 0"}, |
| {"Item 5"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, SanitizePageSizesWhenReparentingItems) { |
| RemoveAllExistingItems(); |
| |
| // Create two pages of apps, where the first page is partial, and the second |
| // page is full and contains a folder. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| const std::string kFolderId = GenerateId("folder_id"); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i <= 33; ++i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| if (i == 15) { |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId, "Folder", "", GetLastPositionString(), "", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| i >= 15 && i < 20 ? kFolderId : "", |
| GetLastPositionString(), kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Note that items Item 15 - Item 19 are in the folder. |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", |
| "Item 6", "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 11", "Item 12", "Item 13", "Item 14", |
| "Folder", "Item 20", "Item 21", "Item 22", "Item 23", |
| "Item 24", "Item 25", "Item 26", "Item 27", "Item 28", |
| "Item 29", "Item 30", "Item 31", "Item 32", "Item 33"}})); |
| |
| // Move an item from the folder to second page. |
| ash::AppListItem* item_12 = FindItemForApp(initial_apps[12].get()); |
| ASSERT_TRUE(item_12); |
| ash::AppListItem* item_11 = FindItemForApp(initial_apps[11].get()); |
| ASSERT_TRUE(item_11); |
| syncer::StringOrdinal target_position = |
| item_12->position().CreateBetween(item_11->position()); |
| |
| ash::AppListItem* item_15 = FindItemForApp(initial_apps[15].get()); |
| ASSERT_TRUE(item_15); |
| |
| GetModelUpdater()->model_for_test()->MoveItemToRootAt(item_15, |
| target_position); |
| |
| // Verify that the last item from the second page moves to a new page. |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", |
| "Item 6", "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 11", "Item 15", "Item 12", "Item 13", |
| "Item 14", "Folder", "Item 20", "Item 21", "Item 22", |
| "Item 23", "Item 24", "Item 25", "Item 26", "Item 27", |
| "Item 28", "Item 29", "Item 30", "Item 31", "Item 32"}, |
| {"Item 33"}})); |
| |
| // Move an item from the folder to the first page, and verify the page count |
| // remains the same. |
| ash::AppListItem* item_1 = FindItemForApp(initial_apps[1].get()); |
| ASSERT_TRUE(item_1); |
| ash::AppListItem* item_2 = FindItemForApp(initial_apps[2].get()); |
| ASSERT_TRUE(item_2); |
| target_position = item_2->position().CreateBetween(item_1->position()); |
| |
| ash::AppListItem* item_16 = FindItemForApp(initial_apps[16].get()); |
| ASSERT_TRUE(item_16); |
| |
| GetModelUpdater()->model_for_test()->MoveItemToRootAt(item_16, |
| target_position); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 16", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 11", "Item 15", "Item 12", "Item 13", |
| "Item 14", "Folder", "Item 20", "Item 21", "Item 22", |
| "Item 23", "Item 24", "Item 25", "Item 26", "Item 27", |
| "Item 28", "Item 29", "Item 30", "Item 31", "Item 32"}, |
| {"Item 33"}})); |
| |
| // Move an item from second page to the folder, and verify the item from the |
| // last page moves back to the second page. |
| GetModelUpdater()->model_for_test()->MergeItems(kFolderId, |
| initial_apps[20]->id()); |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 16", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 11", "Item 15", "Item 12", "Item 13", |
| "Item 14", "Folder", "Item 21", "Item 22", "Item 23", |
| "Item 24", "Item 25", "Item 26", "Item 27", "Item 28", |
| "Item 29", "Item 30", "Item 31", "Item 32", "Item 33"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| NonInstalledItemsIgnoredWhenSanitizingPageSizes) { |
| RemoveAllExistingItems(); |
| |
| // Create two pages of apps, where the first page is partial, and the second |
| // page contains items not installed locally and enough installed apps to just |
| // fill out the second page. |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| const std::string kFolderId = GenerateId("folder_id"); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i <= 41; ++i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| if (i == 15) { |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId, "Folder", "", GetLastPositionString(), "", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| i >= 15 && i < 20 ? kFolderId : "", |
| GetLastPositionString(), kUnset)); |
| if (i % 3) { |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Note that items Item 15 - Item 19 are in the folder. And items with index |
| // divisible by 3 are not installed, and don't count towards the total page |
| // size. |
| ASSERT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", |
| "Item 6", "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 11", "Item 12", "Item 13", "Item 14", "Folder", |
| "Item 20", "Item 21", "Item 22", "Item 23", "Item 24", "Item 25", |
| "Item 26", "Item 27", "Item 28", "Item 29", "Item 30", "Item 31", |
| "Item 32", "Item 33", "Item 34", "Item 35", "Item 36", "Item 37", |
| "Item 38", "Item 39", "Item 40", "Item 41"}})); |
| |
| auto get_app_for_item_index = |
| [&initial_apps](int i) -> extensions::Extension* { |
| DCHECK(i % 3); |
| // Assumes that `initial_apps` contains items whose index is not |
| // divisible by 3. |
| return initial_apps[i - i / 3 - 1].get(); |
| }; |
| |
| // Move an item from first page to the second page - verify the page break is |
| // added so the second page contains only 20 installed items. |
| ash::AppListItem* item_10 = FindItemForApp(get_app_for_item_index(10)); |
| ASSERT_TRUE(item_10); |
| ash::AppListItem* item_11 = FindItemForApp(get_app_for_item_index(11)); |
| ASSERT_TRUE(item_11); |
| syncer::StringOrdinal target_position = |
| item_10->position().CreateBetween(item_11->position()); |
| |
| GetModelUpdater()->RequestPositionUpdate( |
| get_app_for_item_index(2)->id(), target_position, |
| ash::RequestPositionUpdateReason::kMoveItem); |
| |
| ASSERT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 3", "Item 4", "Item 5", "Item 6", |
| "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 2", "Item 11", "Item 12", "Item 13", "Item 14", |
| "Folder", "Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25", "Item 26", "Item 27", "Item 28", "Item 29", "Item 30", |
| "Item 31", "Item 32", "Item 33", "Item 34", "Item 35", "Item 36", |
| "Item 37", "Item 38", "Item 39", "Item 40"}, |
| {"Item 41"}})); |
| |
| // Move an app from the folder to the second page. |
| ash::AppListItem* item_2 = FindItemForApp(get_app_for_item_index(2)); |
| ASSERT_TRUE(item_2); |
| target_position = item_10->position().CreateBetween(item_2->position()); |
| ash::AppListItem* item_16 = FindItemForApp(get_app_for_item_index(16)); |
| ASSERT_TRUE(item_16); |
| |
| GetModelUpdater()->model_for_test()->MoveItemToRootAt(item_16, |
| target_position); |
| |
| ASSERT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 3", "Item 4", "Item 5", "Item 6", |
| "Item 7", "Item 8", "Item 9"}, |
| {"Item 10", "Item 16", "Item 2", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Folder", "Item 20", "Item 21", "Item 22", "Item 23", |
| "Item 24", "Item 25", "Item 26", "Item 27", "Item 28", "Item 29", |
| "Item 30", "Item 31", "Item 32", "Item 33", "Item 34", "Item 35", |
| "Item 36", "Item 37", "Item 38", "Item 39"}, |
| {"Item 40", "Item 41"}})); |
| |
| // Move another app to the second page. |
| target_position = item_10->position().CreateBetween(item_16->position()); |
| GetModelUpdater()->RequestPositionUpdate( |
| get_app_for_item_index(4)->id(), target_position, |
| ash::RequestPositionUpdateReason::kMoveItem); |
| |
| EXPECT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 3", "Item 5", "Item 6", "Item 7", |
| "Item 8", "Item 9"}, |
| {"Item 10", "Item 4", "Item 16", "Item 2", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Folder", "Item 20", "Item 21", "Item 22", |
| "Item 23", "Item 24", "Item 25", "Item 26", "Item 27", "Item 28", |
| "Item 29", "Item 30", "Item 31", "Item 32", "Item 33", "Item 34", |
| "Item 35", "Item 36", "Item 37"}, |
| {"Item 38", "Item 39", "Item 40", "Item 41"}})); |
| |
| // Remove three items from the second page, and verify items from the third |
| // page get moved to the second page. |
| GetModelUpdater()->model_for_test()->MergeItems( |
| kFolderId, get_app_for_item_index(20)->id()); |
| RemoveExtension(get_app_for_item_index(22)->id()); |
| RemoveExtension(get_app_for_item_index(28)->id()); |
| |
| EXPECT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 3", "Item 5", "Item 6", "Item 7", |
| "Item 8", "Item 9"}, |
| {"Item 10", "Item 4", "Item 16", "Item 2", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Folder", "Item 21", "Item 23", "Item 24", |
| "Item 25", "Item 26", "Item 27", "Item 29", "Item 30", "Item 31", |
| "Item 32", "Item 33", "Item 34", "Item 35", "Item 36", "Item 37", |
| "Item 38", "Item 39", "Item 40", "Item 41"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| DontDuplicatePageBreakBetweenUninstalledItems) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i <= 25; ++i) { |
| if (i == 20) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, "", |
| GetLastPositionString(), kUnset)); |
| if (i < 19 || i > 22) { |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", |
| "Item 10", "Item 11", "Item 12", "Item 13", "Item 14", |
| "Item 15", "Item 16", "Item 17", "Item 18", "Item 19"}, |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25"}})); |
| |
| // Install an app, and verify Item 19, which is not installed locally does not |
| // get moved to a new page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| ASSERT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", "Item 16", |
| "Item 17", "Item 18", "Item 19"}, |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25"}})); |
| |
| // Install another app, and verify that Items 18 and 19 get moved to a new |
| // page (as number of installed apps on the first page would otherwise |
| // overflow legacy max page size). |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", |
| "Item 8", "Item 9", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17"}, |
| {"Item 18", "Item 19"}, |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25"}})); |
| |
| // Remove an app from the first page, and verify page brake before Item 18 is |
| // removed, as it can fit into the first page again. |
| RemoveExtension(initial_apps[3]->id()); |
| ASSERT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18", |
| "Item 19"}, |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25"}})); |
| |
| // Remove enough apps so Item 20 can fit into the first page, and verify the |
| // page break before Item 20 does not get removed, as it's treated as a page |
| // brake added via explicit user action in pre-productivity launcher app list. |
| RemoveExtension(initial_apps[4]->id()); |
| RemoveExtension(initial_apps[5]->id()); |
| |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", "Item 11", |
| "Item 12", "Item 13", "Item 14", "Item 15", "Item 16", |
| "Item 17", "Item 18", "Item 19"}, |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25"}})); |
| } |
| |
| // Verifies that app list model sanitizer gracefully handles the case when page |
| // break has to be added between sync items that have duplicate item ordinals. |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesDuplicateOrdinalsAtPageBreakLocation) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string; |
| for (int i = 0; i < 30; ++i) { |
| if (i < 18 || i >= 22) |
| last_position_string = GetLastPositionString(); |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| // Verify a page break was added to sync data. |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(11u, items_per_page[1].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 18), |
| std::vector<std::string>( |
| {"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", "Item 16"})); |
| |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[1].begin() + 3, |
| items_per_page[1].end()), |
| std::vector<std::string>({"Item 22", "Item 23", "Item 24", "Item 25", |
| "Item 26", "Item 27", "Item 28", "Item 29"})); |
| |
| // Installing another app will not create a new page - the last item from |
| // first page will instead be moved to the second page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| items_per_page = GetNamesOfSortedItemsPerPageFromSyncableService(); |
| // Verify a page break was added to sync data. |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(12u, items_per_page[1].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ(std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 19), |
| std::vector<std::string>( |
| {"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16"})); |
| |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[1].begin() + 4, |
| items_per_page[1].end()), |
| std::vector<std::string>({"Item 22", "Item 23", "Item 24", "Item 25", |
| "Item 26", "Item 27", "Item 28", "Item 29"})); |
| } |
| |
| // Verifies that app list model sanitizer gracefully handles the case when page |
| // break has to be added between sync items that have duplicate item ordinals, |
| // where all trailing items have the same ordinal. |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesTrailingDuplicateOrdinals) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string; |
| for (int i = 0; i < 30; ++i) { |
| if (i < 18) |
| last_position_string = GetLastPositionString(); |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| // Verify a page break was added to sync data. |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(11u, items_per_page[1].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 18), |
| std::vector<std::string>( |
| {"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", "Item 16"})); |
| |
| // Installing another app will not create a new page - the last item from |
| // first page will instead be moved to the second page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| items_per_page = GetNamesOfSortedItemsPerPageFromSyncableService(); |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(12u, items_per_page[1].size()); |
| EXPECT_EQ(std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 19), |
| std::vector<std::string>( |
| {"Test app 2", "Test app 1", "Item 0", "Item 1", "Item 2", |
| "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", |
| "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesDuplicateOrdinalsAtTwoBreaks) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string; |
| for (int i = 0; i < 45; ++i) { |
| if (i < 18 || (i >= 25 && i < 35) || i >= 42) |
| last_position_string = GetLastPositionString(); |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| EXPECT_EQ(3u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(20u, items_per_page[1].size()); |
| EXPECT_EQ(6u, items_per_page[2].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 18), |
| std::vector<std::string>( |
| {"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", "Item 16"})); |
| EXPECT_EQ(std::vector<std::string>(items_per_page[1].begin() + 6, |
| items_per_page[1].begin() + 15), |
| std::vector<std::string>({"Item 25", "Item 26", "Item 27", |
| "Item 28", "Item 29", "Item 30", |
| "Item 31", "Item 32", "Item 33"})); |
| EXPECT_EQ(std::vector<std::string>(items_per_page[2].begin() + 3, |
| items_per_page[2].end()), |
| std::vector<std::string>({"Item 42", "Item 43", "Item 44"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesFullPageOfDuplicateOrdinals) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string; |
| for (int i = 0; i < 45; ++i) { |
| if (i < 18 || i >= 42) |
| last_position_string = GetLastPositionString(); |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| // Verify a page break was added to sync data. |
| EXPECT_EQ(3u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(20u, items_per_page[1].size()); |
| EXPECT_EQ(6u, items_per_page[2].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ( |
| std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 18), |
| std::vector<std::string>( |
| {"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", "Item 16"})); |
| EXPECT_EQ(std::vector<std::string>(items_per_page[2].begin() + 3, |
| items_per_page[2].end()), |
| std::vector<std::string>({"Item 42", "Item 43", "Item 44"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesPairOfDuplicateOrdinals) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string; |
| for (int i = 0; i < 25; ++i) { |
| if (i != 19) |
| last_position_string = GetLastPositionString(); |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| // Verify a page break was added to sync data. |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(6u, items_per_page[1].size()); |
| |
| // Verify that the apps whose order is well defined (i.e. that don't have |
| // identical string ordinals) is preserved. |
| EXPECT_EQ(std::vector<std::string>(items_per_page[0].begin(), |
| items_per_page[0].begin() + 19), |
| std::vector<std::string>( |
| {"Test app 1", "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", |
| "Item 11", "Item 12", "Item 13", "Item 14", "Item 15", |
| "Item 16", "Item 17"})); |
| EXPECT_EQ(std::vector<std::string>(items_per_page[1].begin() + 1, |
| items_per_page[1].end()), |
| std::vector<std::string>( |
| {"Item 20", "Item 21", "Item 22", "Item 23", "Item 24"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreakSanitizationHandlesAllDuplicateOrdinals) { |
| RemoveAllExistingItems(); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| std::string last_position_string = GetLastPositionString(); |
| for (int i = 0; i < 30; ++i) { |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| /*folder_id=*/"", |
| last_position_string, kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| // Install another app - with productivity launcher enabled, the app gets |
| // added to front. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_1 = |
| MakeApp("Test app 1", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_1.get()); |
| |
| // Verify a page break was added to sync data. |
| std::vector<std::vector<std::string>> items_per_page = |
| GetNamesOfSortedItemsPerPageFromSyncableService(); |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(11u, items_per_page[1].size()); |
| EXPECT_EQ("Test app 1", items_per_page[0][0]); |
| |
| // Installing another app will not create a new page - the last item from |
| // first page will instead be moved to the second page. |
| last_item_id = CreateNextAppId(last_item_id); |
| scoped_refptr<extensions::Extension> test_app_2 = |
| MakeApp("Test app 2", last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(test_app_2.get()); |
| |
| items_per_page = GetNamesOfSortedItemsPerPageFromSyncableService(); |
| EXPECT_EQ(2u, items_per_page.size()); |
| EXPECT_EQ(20u, items_per_page[0].size()); |
| EXPECT_EQ(12u, items_per_page[1].size()); |
| EXPECT_EQ("Test app 2", items_per_page[0][0]); |
| EXPECT_EQ("Test app 1", items_per_page[0][1]); |
| } |
| |
| // Verifies that sorting works for the mixture of valid and invalid positions. |
| TEST_F(AppListSyncableServiceTest, SortMixedPositionValidityItems) { |
| RemoveAllExistingItems(); |
| |
| using SyncItem = AppListSyncableService::SyncItem; |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| auto item1 = std::make_unique<SyncItem>( |
| kItemId1, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/false); |
| item1->item_name = "a"; |
| item1->item_ordinal = syncer::StringOrdinal(GetLastPositionString()); |
| |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| auto item2 = std::make_unique<SyncItem>( |
| kItemId2, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/true); |
| item2->item_name = "b"; |
| |
| const std::string kItemId3 = CreateNextAppId(kItemId2); |
| auto item3 = std::make_unique<SyncItem>( |
| kItemId3, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/false); |
| item3->item_name = "c"; |
| |
| std::vector<std::unique_ptr<SyncItem>> sync_items; |
| sync_items.push_back(std::move(item1)); |
| sync_items.push_back(std::move(item2)); |
| sync_items.push_back(std::move(item3)); |
| |
| // Populate items and verify their validity. Only `item1` has the valid |
| // position. |
| app_list_syncable_service()->PopulateSyncItemsForTest(std::move(sync_items)); |
| EXPECT_TRUE(app_list_syncable_service() |
| ->GetSyncItem(kItemId1) |
| ->item_ordinal.IsValid()); |
| EXPECT_FALSE(app_list_syncable_service() |
| ->GetSyncItem(kItemId2) |
| ->item_ordinal.IsValid()); |
| EXPECT_FALSE(app_list_syncable_service() |
| ->GetSyncItem(kItemId3) |
| ->item_ordinal.IsValid()); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>({kItemId3, kItemId2, kItemId1})); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>({kItemId1, kItemId2, kItemId3})); |
| } |
| |
| // Verifies that sorting works if all item positions are invalid. |
| TEST_F(AppListSyncableServiceTest, SortInvalidPositionItems) { |
| RemoveAllExistingItems(); |
| |
| using SyncItem = AppListSyncableService::SyncItem; |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| auto item1 = std::make_unique<SyncItem>( |
| kItemId1, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/false); |
| item1->item_name = "a"; |
| |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| auto item2 = std::make_unique<SyncItem>( |
| kItemId2, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/false); |
| item2->item_name = "b"; |
| |
| const std::string kItemId3 = CreateNextAppId(kItemId2); |
| auto item3 = std::make_unique<SyncItem>( |
| kItemId3, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/true); |
| item3->item_name = "c"; |
| |
| std::vector<std::unique_ptr<SyncItem>> sync_items; |
| sync_items.push_back(std::move(item1)); |
| sync_items.push_back(std::move(item2)); |
| sync_items.push_back(std::move(item3)); |
| |
| // Verify the validity of sync item positions. |
| app_list_syncable_service()->PopulateSyncItemsForTest(std::move(sync_items)); |
| EXPECT_FALSE(app_list_syncable_service() |
| ->GetSyncItem(kItemId1) |
| ->item_ordinal.IsValid()); |
| EXPECT_FALSE(app_list_syncable_service() |
| ->GetSyncItem(kItemId2) |
| ->item_ordinal.IsValid()); |
| EXPECT_FALSE(app_list_syncable_service() |
| ->GetSyncItem(kItemId3) |
| ->item_ordinal.IsValid()); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>({kItemId3, kItemId2, kItemId1})); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>({kItemId1, kItemId2, kItemId3})); |
| } |
| |
| // Verifies that sorting with alphateical order works as expected for both |
| // folder items and app items. |
| TEST_F(AppListSyncableServiceTest, VerifyAlphabeticalOrderForFolderItems) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| |
| // Add two apps. |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId1, "a", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId2, "b", "", GetLastPositionString(), kUnset)); |
| |
| // Add one folder containing two apps. |
| const std::string kFolderId1 = GenerateId("FolderId1"); |
| const std::string kChildItemId1_1 = CreateNextAppId(kItemId2); |
| const std::string kChildItemId1_2 = CreateNextAppId(kChildItemId1_1); |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId1, "Folder1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId1_1, "folder1_child1", |
| kFolderId1, GetLastPositionString(), |
| kUnset)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId1_2, "folder1_child2", |
| kFolderId1, GetLastPositionString(), |
| kUnset)); |
| |
| // Add one folder containing three apps. |
| const std::string kFolderId2 = GenerateId("FolderId2"); |
| const std::string kChildItemId2_1 = CreateNextAppId(kChildItemId1_2); |
| const std::string kChildItemId2_2 = CreateNextAppId(kChildItemId2_1); |
| const std::string kChildItemId2_3 = CreateNextAppId(kChildItemId2_2); |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId2, "Folder2", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId2_1, "folder2_child1", |
| kFolderId2, GetLastPositionString(), |
| kUnset)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId2_2, "folder2_child2", |
| kFolderId2, GetLastPositionString(), |
| kUnset)); |
| sync_list.push_back(CreateAppRemoteData(kChildItemId2_3, "folder2_child3", |
| kFolderId2, GetLastPositionString(), |
| kUnset)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Check the default status before sorting. |
| EXPECT_EQ( |
| GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>({kItemId1, kItemId2, kFolderId1, kChildItemId1_1, |
| kChildItemId1_2, kFolderId2, kChildItemId2_1, |
| kChildItemId2_2, kChildItemId2_3})); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| |
| // Folders should be sorted alphabetically with apps. |
| EXPECT_EQ( |
| GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>( |
| {kChildItemId2_3, kChildItemId2_2, kChildItemId2_1, kFolderId2, |
| kChildItemId1_2, kChildItemId1_1, kFolderId1, kItemId2, kItemId1})); |
| } |
| |
| // Verifies that sorting app items with the alphabetical order should work as |
| // expected. Meanwhile, sorting should incur the minimum orinal changes. |
| TEST_F(AppListSyncableServiceTest, VerifyAlphabeticalOrderSort) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId1, "A", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId2, "B", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId3 = CreateNextAppId(kItemId2); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId3, "C", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId4 = CreateNextAppId(kItemId3); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId4, "D", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId5 = CreateNextAppId(kItemId4); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId5, "E", "", GetLastPositionString(), kUnset)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| // Record the mappings between ids and default ordinals. |
| std::unordered_map<std::string, syncer::StringOrdinal> id_ordinal_mappings; |
| for (const auto& id_item_pair : app_list_syncable_service()->sync_items()) { |
| id_ordinal_mappings[id_item_pair.first] = id_item_pair.second->item_ordinal; |
| } |
| |
| // Sorting in alphabetical order should not change any ordinal. Because apps |
| // are already in order. |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>( |
| {kItemId1, kItemId2, kItemId3, kItemId4, kItemId5})); |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| for (const auto& id_item_pair : app_list_syncable_service()->sync_items()) { |
| EXPECT_EQ(id_ordinal_mappings[id_item_pair.first], |
| id_item_pair.second->item_ordinal); |
| } |
| |
| // Sort in reverse alphabetical order. Verify the app order after sorting. |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>( |
| {kItemId5, kItemId4, kItemId3, kItemId2, kItemId1})); |
| |
| const auto* sync_item = app_list_syncable_service()->GetSyncItem(kItemId4); |
| syncer::SyncChangeList change_list{syncer::SyncChange( |
| FROM_HERE, syncer::SyncChange::ACTION_UPDATE, |
| CreateAppRemoteData(kItemId4, sync_item->item_name, sync_item->parent_id, |
| app_list_syncable_service() |
| ->GetSyncItem(kItemId1) |
| ->item_ordinal.CreateAfter() |
| .ToDebugString(), |
| sync_item->item_pin_ordinal.ToDebugString()))}; |
| app_list_syncable_service()->ProcessSyncChanges(base::Location(), |
| change_list); |
| content::RunAllTasksUntilIdle(); |
| |
| // Move Item 4 to the end. Record the mappings between ids and ordinals. |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>( |
| {kItemId5, kItemId3, kItemId2, kItemId1, kItemId4})); |
| id_ordinal_mappings.clear(); |
| for (const auto& id_item_pair : app_list_syncable_service()->sync_items()) { |
| id_ordinal_mappings[id_item_pair.first] = id_item_pair.second->item_ordinal; |
| } |
| |
| // Sort and then verify the app order. |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(GetOrderedItemIdsFromSyncableService(), |
| std::vector<std::string>( |
| {kItemId5, kItemId4, kItemId3, kItemId2, kItemId1})); |
| |
| // Verify that only Item 4's ordinal changes. |
| for (const auto& id_item_pair : app_list_syncable_service()->sync_items()) { |
| if (id_item_pair.first == kItemId4) { |
| EXPECT_NE(id_ordinal_mappings[id_item_pair.first], |
| id_item_pair.second->item_ordinal); |
| } else { |
| EXPECT_EQ(id_ordinal_mappings[id_item_pair.first], |
| id_item_pair.second->item_ordinal); |
| } |
| } |
| } |
| |
| // Verifies that sorting app items with the alphabetical order should work for |
| // the apps with the duplicate names. |
| TEST_F(AppListSyncableServiceTest, VerifyAlphabeticalSortWithDuplicateNames) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId1, "B", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId2, "A", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId3 = CreateNextAppId(kItemId2); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId3, "C", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId4 = CreateNextAppId(kItemId3); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId4, "D", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId5 = CreateNextAppId(kItemId4); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId5, "C", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId6 = CreateNextAppId(kItemId5); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId6, "A", "", GetLastPositionString(), kUnset)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "A", "B", "C", "C", "D"})); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"D", "C", "C", "B", "A", "A"})); |
| } |
| |
| // Verifies that a new app is placed at the correct place when the launcher is |
| // in (reverse) alphabetical order. |
| TEST_F(AppListSyncableServiceTest, NewAppPlacement) { |
| RemoveAllExistingItems(); |
| EXPECT_EQ(ash::AppListSortOrder::kCustom, GetSortOrderFromPrefs()); |
| |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| const std::string kItemId3 = CreateNextAppId(kItemId2); |
| const std::string kItemId4 = CreateNextAppId(kItemId3); |
| |
| scoped_refptr<extensions::Extension> app1 = |
| MakeApp("A", kItemId1, extensions::Extension::NO_FLAGS); |
| InstallExtension(app1.get()); |
| scoped_refptr<extensions::Extension> app2 = |
| MakeApp("B", kItemId2, extensions::Extension::NO_FLAGS); |
| InstallExtension(app2.get()); |
| scoped_refptr<extensions::Extension> app3 = |
| MakeApp("C", kItemId3, extensions::Extension::NO_FLAGS); |
| InstallExtension(app3.get()); |
| scoped_refptr<extensions::Extension> app4 = |
| MakeApp("E", kItemId4, extensions::Extension::NO_FLAGS); |
| InstallExtension(app4.get()); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameReverseAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameReverseAlphabetical, |
| GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"E", "C", "B", "A"})); |
| |
| // Insert another app. Verify the order. |
| const std::string kItemId5 = CreateNextAppId(kItemId4); |
| scoped_refptr<extensions::Extension> app5 = |
| MakeApp("D", kItemId5, extensions::Extension::NO_FLAGS); |
| InstallExtension(app5.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"E", "D", "C", "B", "A"})); |
| |
| // The longest subsequence in reverse alphabetical order is the whole |
| // sequence. Therefore the entropy is (1 - 5/5), which is 0. |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| EXPECT_TRUE(cc::MathUtil::IsWithinEpsilon( |
| 0.f, |
| reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameReverseAlphabetical, model_updater))); |
| |
| // The longest subsequence in alphabetical order has one element so the |
| // entropy is (1 - 1/5) which is 0.8. |
| EXPECT_EQ(0.8f, reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameAlphabetical, model_updater)); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "D", "E"})); |
| |
| ChangeItemName(kItemId3, "Z"); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "Z", "D", "E"})); |
| |
| // The longest subsequence in order is ["A", "B", "D", "E"] so the entropy is |
| // (1 - 4/5) which is 0.2. |
| EXPECT_TRUE(cc::MathUtil::IsWithinEpsilon( |
| 0.2f, reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameAlphabetical, model_updater))); |
| |
| // Install a new app. Verify its location. |
| const std::string kItemId6 = CreateNextAppId(kItemId5); |
| scoped_refptr<extensions::Extension> app6 = |
| MakeApp("C", kItemId6, extensions::Extension::NO_FLAGS); |
| InstallExtension(app6.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "Z", "D", "E"})); |
| |
| // Change another app's name. |
| ChangeItemName(kItemId2, "F"); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "F", "C", "Z", "D", "E"})); |
| |
| // The longest subsequence in order is ["A", "C", "D", "E"] so the entropy is |
| // (1 - 4/6). |
| EXPECT_TRUE(cc::MathUtil::IsWithinEpsilon( |
| 1 / 3.f, reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameAlphabetical, model_updater))); |
| |
| // Install a new app. |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| const std::string kItemId7 = CreateNextAppId(kItemId6); |
| scoped_refptr<extensions::Extension> app7 = |
| MakeApp("G", kItemId7, extensions::Extension::NO_FLAGS); |
| InstallExtension(app7.get()); |
| |
| // The entropy is too high so the new app is inserted at the front. |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"G", "A", "F", "C", "Z", "D", "E"})); |
| |
| // The sort order is reset. |
| EXPECT_EQ(ash::AppListSortOrder::kCustom, GetSortOrderFromPrefs()); |
| |
| // Install a new app. |
| const std::string kItemId8 = CreateNextAppId(kItemId7); |
| scoped_refptr<extensions::Extension> app8 = |
| MakeApp("H", kItemId8, extensions::Extension::NO_FLAGS); |
| InstallExtension(app8.get()); |
| |
| // Because the sort order is kCustom, the new app is placed at the front. |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"H", "G", "A", "F", "C", "Z", "D", "E"})); |
| } |
| |
| // Verifies that a new app is placed at the correct place when initially all of |
| // top level items are folders. |
| TEST_F(AppListSyncableServiceTest, NewAppPlacementInitiallyOnlyFolders) { |
| RemoveAllExistingItems(); |
| |
| // Add three folders. |
| syncer::StringOrdinal position = |
| syncer::StringOrdinal::CreateInitialOrdinal(); |
| const std::string kFolderItemId1 = GenerateId("folder_id1"); |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| std::unique_ptr<ChromeAppListItem> folder_item1 = |
| std::make_unique<ChromeAppListItem>(profile_.get(), kFolderItemId1, |
| model_updater); |
| folder_item1->SetChromeIsFolder(true); |
| ItemTestApi(folder_item1.get()).SetPosition(position); |
| ItemTestApi(folder_item1.get()).SetName("Folder1"); |
| app_list_syncable_service()->AddItem(std::move(folder_item1)); |
| position = position.CreateBefore(); |
| |
| const std::string kFolderItemId2 = GenerateId("folder_id2"); |
| std::unique_ptr<ChromeAppListItem> folder_item2 = |
| std::make_unique<ChromeAppListItem>(profile_.get(), kFolderItemId2, |
| model_updater); |
| folder_item2->SetChromeIsFolder(true); |
| ItemTestApi(folder_item2.get()).SetPosition(position); |
| ItemTestApi(folder_item2.get()).SetName("Folder2"); |
| app_list_syncable_service()->AddItem(std::move(folder_item2)); |
| position = position.CreateBefore(); |
| |
| const std::string kFolderItemId3 = GenerateId("folder_id3"); |
| std::unique_ptr<ChromeAppListItem> folder_item3 = |
| std::make_unique<ChromeAppListItem>(profile_.get(), kFolderItemId3, |
| model_updater); |
| folder_item3->SetChromeIsFolder(true); |
| ItemTestApi(folder_item3.get()).SetPosition(position); |
| // Use an empty folder name. Note that empty folder name will be interpreted |
| // as "Unnamed" during sorting, which is the same as the name showing to the |
| // users. |
| ItemTestApi(folder_item3.get()).SetName(""); |
| app_list_syncable_service()->AddItem(std::move(folder_item3)); |
| |
| // Sort sync items then verify the item order. |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"Folder1", "Folder2", ""})); |
| |
| // Install a new app. |
| const std::string kNewAppId = CreateNextAppId(GenerateId("app_id")); |
| scoped_refptr<extensions::Extension> app = |
| MakeApp("G", kNewAppId, extensions::Extension::NO_FLAGS); |
| InstallExtension(app.get()); |
| |
| // Verify that the app is placed alphabetically - note that empty folder name |
| // is treated as "Unnamed". |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"Folder1", "Folder2", "G", ""})); |
| |
| // Verify that the entropy is zero. |
| EXPECT_TRUE(cc::MathUtil::IsWithinEpsilon( |
| 0.f, reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameAlphabetical, model_updater))); |
| |
| // Install the second app. |
| const std::string kNewAppId2 = CreateNextAppId(GenerateId("app_id2")); |
| scoped_refptr<extensions::Extension> app2 = |
| MakeApp("c", kNewAppId2, extensions::Extension::NO_FLAGS); |
| InstallExtension(app2.get()); |
| |
| // Verify that the app is placed alphabetically again. |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"c", "Folder1", "Folder2", "G", ""})); |
| |
| // Change folders' names so that folders are out of order. |
| ChangeItemName(kFolderItemId1, "Folder2"); |
| ChangeItemName(kFolderItemId2, "Folder1"); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"c", "Folder2", "Folder1", "G", ""})); |
| |
| // There is one folder item out of order so the entropy should be 1/5 = 0.2. |
| EXPECT_TRUE(cc::MathUtil::IsWithinEpsilon( |
| 0.2f, reorder::CalculateEntropyForTest( |
| ash::AppListSortOrder::kNameAlphabetical, model_updater))); |
| |
| // Install the third app. Verify the item order after installation. |
| const std::string kNewAppId3 = CreateNextAppId(GenerateId("app_id3")); |
| scoped_refptr<extensions::Extension> app3 = |
| MakeApp("Fs", kNewAppId3, extensions::Extension::NO_FLAGS); |
| InstallExtension(app3.get()); |
| EXPECT_EQ( |
| GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"c", "Folder2", "Folder1", "Fs", "G", ""})); |
| |
| // Install the forth app. Verify that the new item is inserted in alphabetical |
| // order. |
| const std::string kNewAppId4 = CreateNextAppId(GenerateId("app_id4")); |
| scoped_refptr<extensions::Extension> app4 = |
| MakeApp("z", kNewAppId4, extensions::Extension::NO_FLAGS); |
| InstallExtension(app4.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>( |
| {"c", "Folder2", "Folder1", "Fs", "G", "", "z"})); |
| } |
| |
| // Verifies that the new app's position maintains the launcher sort order among |
| // sync items (including the apps not enabled on the local device). |
| TEST_F(AppListSyncableServiceTest, VerifyNewAppPositionInGlobalScope) { |
| RemoveAllExistingItems(); |
| |
| const std::string kItemId1 = CreateNextAppId(GenerateId("app_id1")); |
| scoped_refptr<extensions::Extension> app1 = |
| MakeApp("C", kItemId1, extensions::Extension::NO_FLAGS); |
| InstallExtension(app1.get()); |
| |
| const std::string kItemId2 = CreateNextAppId(GenerateId("app_id2")); |
| scoped_refptr<extensions::Extension> app2 = |
| MakeApp("A", kItemId2, extensions::Extension::NO_FLAGS); |
| InstallExtension(app2.get()); |
| |
| const std::string kItemId3 = CreateNextAppId(GenerateId("app_id3")); |
| scoped_refptr<extensions::Extension> app3 = |
| MakeApp("D", kItemId3, extensions::Extension::NO_FLAGS); |
| InstallExtension(app3.get()); |
| |
| // The sort order is not set. Therefore an app is always placed at the front. |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"D", "A", "C"})); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "C", "D"})); |
| |
| // A hacky way to emulate that an item is disabled locally (in other words, |
| // the app's sync data exists but its app list item data is missing). |
| AppListModelUpdater* model_updater = GetModelUpdater(); |
| model_updater->RemoveItem(kItemId1, /*is_uninstall=*/true); |
| |
| // Install a new app and verify the app order. |
| const std::string kItemId4 = CreateNextAppId(GenerateId("app_id4")); |
| scoped_refptr<extensions::Extension> app4 = |
| MakeApp("B", kItemId4, extensions::Extension::NO_FLAGS); |
| InstallExtension(app4.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "D"})); |
| |
| // Remove another item from the model. Now only "A" and "B" are in the model. |
| model_updater->RemoveItem(kItemId3, /*is_uninstall=*/true); |
| |
| // Install a new app and verify the app order. |
| const std::string kItemId5 = CreateNextAppId(GenerateId("app_id5")); |
| scoped_refptr<extensions::Extension> app5 = |
| MakeApp("F", kItemId5, extensions::Extension::NO_FLAGS); |
| InstallExtension(app5.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "D", "F"})); |
| |
| // Remove another item from the model. Now only "A" and "B" are in the model. |
| model_updater->RemoveItem(kItemId5, /*is_uninstall=*/true); |
| |
| // Install a new app and verify the app order. |
| const std::string kItemId6 = CreateNextAppId(GenerateId("app_id6")); |
| scoped_refptr<extensions::Extension> app6 = |
| MakeApp("E", kItemId6, extensions::Extension::NO_FLAGS); |
| InstallExtension(app6.get()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "D", "E", "F"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, RemovePageBreaksIfAppsDontFillUpAPage) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| const std::string kItemId1 = CreateNextAppId(extensions::kWebStoreAppId); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId1, "B", "", GetLastPositionString(), kUnset)); |
| const std::string kItemId2 = CreateNextAppId(kItemId1); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId2, "A", "", GetLastPositionString(), kUnset)); |
| const std::string kPageBreak1 = CreateNextAppId(kItemId2); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreak1, "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| const std::string kItemId3 = CreateNextAppId(kPageBreak1); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId3, "C", "", GetLastPositionString(), kUnset)); |
| const std::string kPageBreak2 = CreateNextAppId(kItemId3); |
| sync_list.push_back(CreateAppRemoteData( |
| kPageBreak2, "page_break_2", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| const std::string kItemId4 = CreateNextAppId(kPageBreak2); |
| sync_list.push_back( |
| CreateAppRemoteData(kItemId4, "D", "", GetLastPositionString(), kUnset)); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| scoped_refptr<extensions::Extension> app1 = |
| MakeApp("B", kItemId1, extensions::Extension::NO_FLAGS); |
| InstallExtension(app1.get()); |
| |
| scoped_refptr<extensions::Extension> app3 = |
| MakeApp("C", kItemId3, extensions::Extension::NO_FLAGS); |
| InstallExtension(app1.get()); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetOrderedNamesFromSyncableService(), |
| std::vector<std::string>({"A", "B", "C", "D"})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| RemovePageBreaksIfAppCountMatchesLegacyPageSize) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 0; i < 20; ++i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, "", |
| GetLastPositionString(), kUnset)); |
| if (i % 2) { |
| scoped_refptr<extensions::Extension> app = |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(app.get()); |
| } |
| } |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17", |
| "Item 18", "Item 19", "Item 2", "Item 3", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, PageBreaksAfterSortWithTwoPagesInSync) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 24; i >= 0; --i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, "", |
| GetLastPositionString(), kUnset)); |
| scoped_refptr<extensions::Extension> app = |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(app.get()); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17", |
| "Item 18", "Item 19", "Item 2", "Item 20", "Item 21", |
| "Item 22", "Item 23", "Item 24", "Item 3", "Item 4"}, |
| {"Item 5", "Item 6", "Item 7", "Item 8", "Item 9"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreaksAfterSortWithTwoPagesAndNonInstalledItemsInSync) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 33; i >= 0; --i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, "", |
| GetLastPositionString(), kUnset)); |
| scoped_refptr<extensions::Extension> app = |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS); |
| |
| // Leave subset of apps non-installed. |
| if (i % 3) |
| InstallExtension(app.get()); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ( |
| GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 10", "Item 11", "Item 12", "Item 13", |
| "Item 14", "Item 15", "Item 16", "Item 17", "Item 18", "Item 19", |
| "Item 2", "Item 20", "Item 21", "Item 22", "Item 23", "Item 24", |
| "Item 25", "Item 26", "Item 27", "Item 28", "Item 29", "Item 3", |
| "Item 30", "Item 31", "Item 32", "Item 33", "Item 4", "Item 5", |
| "Item 6"}, |
| {"Item 7", "Item 8", "Item 9"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, |
| PageBreaksAfterSortWithTwoPagesAndAFolderInSync) { |
| RemoveAllExistingItems(); |
| |
| const std::string kFolderId = GenerateId("folder_id"); |
| |
| std::vector<scoped_refptr<extensions::Extension>> initial_apps; |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 24; i >= 0; --i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| if (i == 20) { |
| sync_list.push_back(CreateAppRemoteData( |
| kFolderId, "Folder", "", GetLastPositionString(), "", |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_FOLDER)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, |
| i >= 15 && i < 20 ? kFolderId : "", |
| GetLastPositionString(), kUnset)); |
| initial_apps.push_back( |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS)); |
| } |
| |
| for (auto app : initial_apps) |
| InstallExtension(app.get()); |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| // Note that items Item 15 - Item 19 are in the folder. |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Folder", "Item 0", "Item 1", "Item 10", "Item 11", |
| "Item 12", "Item 13", "Item 14", "Item 2", "Item 20", |
| "Item 21", "Item 22", "Item 23", "Item 24", "Item 3", |
| "Item 4", "Item 5", "Item 6", "Item 7", "Item 8"}, |
| {"Item 9"}})); |
| } |
| |
| TEST_F(AppListSyncableServiceTest, PageBreaksAfterSortWithTwoFullPagesInSync) { |
| RemoveAllExistingItems(); |
| syncer::SyncDataList sync_list; |
| std::string last_item_id = extensions::kWebStoreAppId; |
| for (int i = 39; i >= 0; --i) { |
| if (i == 10) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_1", "page_break_1", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| if (i == 25) { |
| sync_list.push_back(CreateAppRemoteData( |
| "page_break_2", "page_break_2", "", GetLastPositionString(), kUnset, |
| sync_pb::AppListSpecifics_AppListItemType_TYPE_PAGE_BREAK)); |
| } |
| |
| last_item_id = CreateNextAppId(last_item_id); |
| const std::string item_name = base::StringPrintf("Item %d", i); |
| sync_list.push_back(CreateAppRemoteData(last_item_id, item_name, "", |
| GetLastPositionString(), kUnset)); |
| scoped_refptr<extensions::Extension> app = |
| MakeApp(item_name, last_item_id, extensions::Extension::NO_FLAGS); |
| InstallExtension(app.get()); |
| } |
| |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| content::RunAllTasksUntilIdle(); |
| |
| app_list_syncable_service()->SetAppListPreferredOrder( |
| ash::AppListSortOrder::kNameAlphabetical); |
| EXPECT_EQ(ash::AppListSortOrder::kNameAlphabetical, GetSortOrderFromPrefs()); |
| EXPECT_EQ(GetNamesOfSortedItemsPerPageFromSyncableService(), |
| std::vector<std::vector<std::string>>( |
| {{"Item 0", "Item 1", "Item 10", "Item 11", "Item 12", |
| "Item 13", "Item 14", "Item 15", "Item 16", "Item 17", |
| "Item 18", "Item 19", "Item 2", "Item 20", "Item 21", |
| "Item 22", "Item 23", "Item 24", "Item 25", "Item 26"}, |
| {"Item 27", "Item 28", "Item 29", "Item 3", "Item 30", |
| "Item 31", "Item 32", "Item 33", "Item 34", "Item 35", |
| "Item 36", "Item 37", "Item 38", "Item 39", "Item 4", |
| "Item 5", "Item 6", "Item 7", "Item 8", "Item 9"}})); |
| } |
| |
| // Base class for tests of `AppListSyncableService::OnFirstSync()` parameterized |
| // by whether the first sync in the session is the first sync ever across all |
| // ChromeOS devices and sessions for the associated user. |
| class AppListSyncableServiceOnFirstSyncTest |
| : public AppListSyncableServiceTest, |
| public testing::WithParamInterface< |
| /*first_sync_was_first_sync_ever=*/bool> { |
| public: |
| // Returns whether the first sync in the session is the first sync ever across |
| // all ChromeOS devices and sessions for the associated user given test |
| // parameterization. |
| bool first_sync_was_first_sync_ever() const { return GetParam(); } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| AppListSyncableServiceOnFirstSyncTest, |
| ::testing::Bool()); |
| |
| // Verifies that `AppListSyncableService::OnFirstSync()` runs callbacks at |
| // the expected times and with the expected values. |
| TEST_P(AppListSyncableServiceOnFirstSyncTest, OnFirstSync) { |
| syncer::SyncDataList sync_data_list; |
| |
| // Populate `sync_data_list` when the first sync in the session should *not* |
| // be the first sync ever across all ChromeOS devices and sessions for the |
| // associated user. |
| if (!first_sync_was_first_sync_ever()) { |
| sync_data_list.push_back( |
| CreateAppRemoteData(GenerateId("item_id"), "item_name", |
| GenerateId("parent_id"), "ordinal", "pin_ordinal")); |
| } |
| |
| // Create a test future for a callback to register *before* the first sync |
| // in the session is completed, and another to register *after*. |
| base::test::TestFuture<bool> before_first_sync_future; |
| base::test::TestFuture<bool> after_first_sync_future; |
| |
| // Register a callback *before* the first sync in the session is completed. |
| app_list_syncable_service()->OnFirstSync( |
| before_first_sync_future.GetCallback()); |
| |
| // Complete the first sync in the session. |
| app_list_syncable_service()->MergeDataAndStartSyncing( |
| syncer::APP_LIST, sync_data_list, |
| std::make_unique<syncer::FakeSyncChangeProcessor>()); |
| |
| // Register a callback *after* the first sync in the session is completed. |
| app_list_syncable_service()->OnFirstSync( |
| after_first_sync_future.GetCallback()); |
| |
| // Neither callback should have run since callbacks are posted. |
| EXPECT_FALSE(before_first_sync_future.IsReady()); |
| EXPECT_FALSE(after_first_sync_future.IsReady()); |
| |
| // When run, callbacks should reflect whether the first sync in the session |
| // was the first sync ever across all ChromeOS devices and sessions for the |
| // associated user. |
| EXPECT_EQ(before_first_sync_future.Get(), first_sync_was_first_sync_ever()); |
| EXPECT_EQ(after_first_sync_future.Get(), first_sync_was_first_sync_ever()); |
| } |
| |
| } // namespace app_list |