diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index a14ff8067b5..6ab0f8c638b 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -89,7 +89,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService { case .blazeCampaignObjective: return true case .favoriteProducts: - return false + return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 7f9e27cf2ab..efb904b9640 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -10,6 +10,7 @@ - [internal] Logging for app storage size added in Core Data crash logs [https://github.com/woocommerce/woocommerce-ios/pull/14008] - [*] Payments: Prevent phone from sleeping during card reader updates [https://github.com/woocommerce/woocommerce-ios/pull/14021] - [internal] Fixed concurrency issue with Blaze local notification scheduler. [https://github.com/woocommerce/woocommerce-ios/pull/14012] +- [internal] We added ability to mark and filter favorite products. This feature isn't released yet. But please smoke test product filtering work as expected. [https://github.com/woocommerce/woocommerce-ios/pull/14001] 20.5 ----- diff --git a/WooCommerce/Classes/ViewRelated/Products/FavoriteProducts/FavoriteProductsUseCase.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/FavoriteProducts/FavoriteProductsUseCase.swift similarity index 85% rename from WooCommerce/Classes/ViewRelated/Products/FavoriteProducts/FavoriteProductsUseCase.swift rename to WooCommerce/Classes/ViewRelated/Products/Edit Product/FavoriteProducts/FavoriteProductsUseCase.swift index b525bf0b9a2..1ee45517fac 100644 --- a/WooCommerce/Classes/ViewRelated/Products/FavoriteProducts/FavoriteProductsUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/FavoriteProducts/FavoriteProductsUseCase.swift @@ -1,5 +1,6 @@ import Foundation import Yosemite +import Experiments protocol FavoriteProductsUseCase { func markAsFavorite(productID: Int64) @@ -34,11 +35,14 @@ private extension FavoriteProductsFilter { struct DefaultFavoriteProductsUseCase: FavoriteProductsUseCase { private let siteID: Int64 private let stores: StoresManager + private let featureFlagService: FeatureFlagService init(siteID: Int64, - stores: StoresManager = ServiceLocator.stores) { + stores: StoresManager = ServiceLocator.stores, + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { self.siteID = siteID self.stores = stores + self.featureFlagService = featureFlagService } @MainActor @@ -64,6 +68,9 @@ struct DefaultFavoriteProductsUseCase: FavoriteProductsUseCase { @MainActor func favoriteProductIDs() async -> [Int64] { + guard featureFlagService.isFeatureFlagEnabled(.favoriteProducts) else { + return [] + } return await withCheckedContinuation { continuation in stores.dispatch(AppSettingsAction.loadFavoriteProductIDs(siteID: siteID, onCompletion: { savedFavProductIDs in continuation.resume(returning: savedFavProductIDs) diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index 373f32a3411..8e8bf83f772 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -1110,6 +1110,7 @@ private extension ProductFormViewController { switch result { case .success: ServiceLocator.analytics.track(.productDetailProductDeleted) + self.viewModel.removeFromFavorite() // Dismisses the in-progress UI. self.navigationController?.dismiss(animated: true, completion: nil) // Dismiss or Pop the Product Form diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductSelector/ProductSelectorViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/ProductSelector/ProductSelectorViewModel.swift index 9b75e811a61..d27419e977a 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductSelector/ProductSelectorViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductSelector/ProductSelectorViewModel.swift @@ -232,6 +232,9 @@ final class ProductSelectorViewModel: ObservableObject { @Published var productVariationListViewModel: ProductVariationSelectorViewModel? = nil + private let favoriteProductsUseCase: FavoriteProductsUseCase + private(set) var favoriteProductIDs: [Int64] = [] + init(siteID: Int64, source: ProductSelectorSource, selectedItemIDs: [Int64] = [], @@ -241,6 +244,7 @@ final class ProductSelectorViewModel: ObservableObject { stores: StoresManager = ServiceLocator.stores, analytics: Analytics = ServiceLocator.analytics, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, + favoriteProductsUseCase: FavoriteProductsUseCase? = nil, toggleAllVariationsOnSelection: Bool = true, topProductsProvider: ProductSelectorTopProductsProviderProtocol? = nil, pageFirstIndex: Int = PaginationTracker.Defaults.pageFirstIndex, @@ -279,6 +283,11 @@ final class ProductSelectorViewModel: ObservableObject { self.onConfigureProductRow = onConfigureProductRow tracker = ProductSelectorViewModelTracker(analytics: analytics, trackProductsSource: topProductsProvider != nil) + self.favoriteProductsUseCase = favoriteProductsUseCase ?? DefaultFavoriteProductsUseCase(siteID: siteID) + Task { @MainActor [weak self] in + await self?.loadFavoriteProductIDs() + } + topProductsFromCachedOrders = topProductsProvider?.provideTopProducts(siteID: siteID) ?? .empty tracker.viewModel = self @@ -480,6 +489,13 @@ final class ProductSelectorViewModel: ObservableObject { onAllSelectionsCleared?() } + /// Loads favorite product IDs + /// + @MainActor + func loadFavoriteProductIDs() async { + favoriteProductIDs = await favoriteProductsUseCase.favoriteProductIDs() + } + enum SyncApproach { case external case onButtonTap @@ -511,6 +527,7 @@ extension ProductSelectorViewModel: PaginationTrackerDelegate { productType: filtersSubject.value.promotableProductType?.productType, productCategory: filtersSubject.value.productCategory, sortOrder: .nameAscending, + productIDs: (filtersSubject.value.favoriteProduct != nil) ? favoriteProductIDs : [], shouldDeleteStoredProductsOnFirstPage: shouldDeleteStoredProductsOnFirstPage) { [weak self] result in guard let self = self else { return } @@ -720,11 +737,22 @@ private extension ProductSelectorViewModel { sections.append(ProductSelectorSection(type: type, products: products)) } - func updatePredicate(searchTerm: String, filters: FilterProductListViewModel.Filters, productSearchFilter: ProductSearchFilter) { + func updatePredicate(searchTerm: String, filters: FilterProductListViewModel.Filters, productSearchFilter: ProductSearchFilter) async { + let productIDs: [Int64]? = await { + guard filters.favoriteProduct != nil else { + return nil + } + + await loadFavoriteProductIDs() + return favoriteProductIDs + }() + productsResultsController.updatePredicate(siteID: siteID, stockStatus: filters.stockStatus, productStatus: filters.productStatus, - productType: filters.promotableProductType?.productType) + productType: filters.promotableProductType?.productType, + productIDs: productIDs) + if searchTerm.isNotEmpty { // When the search query changes, also includes the original results predicate in addition to the search keyword and filter key. let searchResultsPredicate = NSPredicate(format: "SUBQUERY(searchResults, $result, $result.keyword = %@ AND $result.filterKey = %@).@count > 0", @@ -770,10 +798,12 @@ private extension ProductSelectorViewModel { Publishers.CombineLatest3(searchTermPublisher, filtersPublisher, searchFilterPublisher) .sink { [weak self] searchTerm, filtersSubject, productSearchFilter in guard let self = self else { return } - self.updateFilterButtonTitle(with: filtersSubject) - self.updatePredicate(searchTerm: searchTerm, filters: filtersSubject, productSearchFilter: productSearchFilter) - self.reloadData() - self.paginationTracker.resync() + Task { @MainActor in + self.updateFilterButtonTitle(with: filtersSubject) + await self.updatePredicate(searchTerm: searchTerm, filters: filtersSubject, productSearchFilter: productSearchFilter) + self.reloadData() + self.paginationTracker.resync() + } }.store(in: &subscriptions) } diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsListViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsListViewModel.swift index 08a3e91ddcf..73bfabf4dda 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsListViewModel.swift @@ -24,15 +24,24 @@ final class ProductListViewModel: ProductsListViewModelProtocol { private let barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder private let featureFlagService: FeatureFlagService + private let favoriteProductsUseCase: FavoriteProductsUseCase + private(set) var favoriteProductIDs: [Int64] = [] + init(siteID: Int64, stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder = BarcodeSKUScannerItemFinder()) { + favoriteProductsUseCase: FavoriteProductsUseCase? = nil, + barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder = BarcodeSKUScannerItemFinder(), + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { self.siteID = siteID self.stores = stores self.featureFlagService = featureFlagService self.wooSubscriptionProductsEligibilityChecker = WooSubscriptionProductsEligibilityChecker(siteID: siteID) self.barcodeSKUScannerItemFinder = barcodeSKUScannerItemFinder + self.favoriteProductsUseCase = favoriteProductsUseCase ?? DefaultFavoriteProductsUseCase(siteID: siteID) + + Task { @MainActor [weak self] in + await self?.loadFavoriteProductIDs() + } } var selectedProductsCount: Int { @@ -208,6 +217,13 @@ final class ProductListViewModel: ProductsListViewModelProtocol { }) } + /// Loads favorite product IDs + /// + @MainActor + func loadFavoriteProductIDs() async { + favoriteProductIDs = await favoriteProductsUseCase.favoriteProductIDs() + } + private func isPluginActive(_ plugin: String, completion: @escaping (Bool) -> (Void)) { let action = SystemStatusAction.fetchSystemPluginListWithNameList(siteID: siteID, systemPluginNameList: [plugin]) { plugin in completion(plugin?.active == true) diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index e2a99f10244..acf22378570 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -162,21 +162,20 @@ final class ProductsViewController: UIViewController, GhostableViewController { private var filters: FilterProductListViewModel.Filters = FilterProductListViewModel.Filters() { didSet { - if filters != oldValue || - categoryHasChangedRemotely { - updateLocalProductSettings(sort: sortOrder, - filters: filters) - updateFilterButtonTitle(filters: filters) + Task { @MainActor in + if filters != oldValue || + categoryHasChangedRemotely { + updateLocalProductSettings(sort: sortOrder, + filters: filters) + updateFilterButtonTitle(filters: filters) - resultsController.updatePredicate(siteID: siteID, - stockStatus: filters.stockStatus, - productStatus: filters.productStatus, - productType: filters.promotableProductType?.productType) + await updatePredicate(filters: filters) - /// Reload because `updatePredicate` calls `performFetch` when creating a new predicate - tableView.reloadData() + /// Reload because `updatePredicate` calls `performFetch` when creating a new predicate + tableView.reloadData() - paginationTracker.resync() + paginationTracker.resync() + } } } } @@ -275,6 +274,8 @@ final class ProductsViewController: UIViewController, GhostableViewController { } navigationController?.navigationBar.removeShadow() + + reloadFavoriteProductsIfNeeded() } override func viewWillDisappear(_ animated: Bool) { @@ -1030,6 +1031,49 @@ private extension ProductsViewController { } return indexPathsForVisibleRows.contains(indexPath) } + + func reloadFavoriteProductsIfNeeded() { + Task { @MainActor [weak self] in + guard let self else { return } + + // Reload only if favorite products filter is turned on + guard filters.favoriteProduct != nil else { + return + } + + let previousFavoriteProductIDs = viewModel.favoriteProductIDs + await viewModel.loadFavoriteProductIDs() + + // Reload only if favorite product IDs changed + guard viewModel.favoriteProductIDs != previousFavoriteProductIDs else { + return + } + + await updatePredicate(filters: filters) + + /// Reload because `updatePredicate` calls `performFetch` when creating a new predicate + tableView.reloadData() + + paginationTracker.resync() + } + } + + func updatePredicate(filters: FilterProductListViewModel.Filters) async { + let productIDs: [Int64]? = await { + guard filters.favoriteProduct != nil else { + return nil + } + + await viewModel.loadFavoriteProductIDs() + return viewModel.favoriteProductIDs + }() + + resultsController.updatePredicate(siteID: siteID, + stockStatus: filters.stockStatus, + productStatus: filters.productStatus, + productType: filters.promotableProductType?.productType, + productIDs: productIDs) + } } // MARK: - UITableViewDataSource Conformance @@ -1175,10 +1219,15 @@ private extension ProductsViewController { // private extension ProductsViewController { @objc private func pullToRefresh(sender: UIRefreshControl) { - ServiceLocator.analytics.track(.productListPulledToRefresh) + Task { @MainActor in - paginationTracker.resync { - sender.endRefreshing() + ServiceLocator.analytics.track(.productListPulledToRefresh) + + await updatePredicate(filters: filters) + + paginationTracker.resync { + sender.endRefreshing() + } } } @@ -1374,7 +1423,8 @@ extension ProductsViewController: PaginationTrackerDelegate { productStatus: filters.productStatus, productType: filters.promotableProductType?.productType, productCategory: filters.productCategory, - sortOrder: sortOrder) { [weak self] result in + sortOrder: sortOrder, + productIDs: (filters.favoriteProduct != nil) ? viewModel.favoriteProductIDs : []) { [weak self] result in guard let self = self else { return } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 5edea469124..cd021f64adb 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -2893,6 +2893,7 @@ EEA1E2042AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E2032AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift */; }; EEA1E20C2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E20B2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift */; }; EEA1E20E2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E20D2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift */; }; + EEA3C2152CA3DB18000E82EC /* DefaultFavoriteProductsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */; }; EEA6935C2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935B2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift */; }; EEA6935E2B231C6600BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935D2B231C6500BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift */; }; EEA693602B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935F2B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift */; }; @@ -5955,6 +5956,7 @@ EEA1E2032AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyProductDetailPreviewViewModelTests.swift; sourceTree = ""; }; EEA1E20B2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductWithAIContainerViewModelTests.swift; sourceTree = ""; }; EEA1E20D2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+ProductCreationAI.swift"; sourceTree = ""; }; + EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultFavoriteProductsUseCaseTests.swift; sourceTree = ""; }; EEA6935B2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyConfirmationView.swift; sourceTree = ""; }; EEA6935D2B231C6500BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyConfirmationViewModel.swift; sourceTree = ""; }; EEA6935F2B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyUseCaseTests.swift; sourceTree = ""; }; @@ -6239,6 +6241,7 @@ 020B2F9723BDF2D000BD79AD /* Edit Product */ = { isa = PBXGroup; children = ( + EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */, CE91BE9E29FBE9BD00B6E1AF /* Quantity Rules */, CCE64E3C29EEA9C500C1C937 /* Subscription */, CC09A91029CB1AB700D6C4AD /* Composite Products */, @@ -6330,6 +6333,7 @@ 0262DA5523A23AA40029AF30 /* Shipping Settings */, 45B9C63B23A8E4DB007FC4C5 /* Edit Price */, 265BCA032430E5EA004E53EE /* Edit Categories */, + EE289AE12C988FF0004AB1A6 /* FavoriteProducts */, 4592A54824BF58A200BC3DE0 /* Edit Tags */, 45E9A6E124DAE18E00A600E8 /* Reviews */, CC01CE5B29B2342E004FF537 /* Bundled Products */, @@ -8897,7 +8901,6 @@ 4592A54824BF58A200BC3DE0 /* Edit Tags */ = { isa = PBXGroup; children = ( - EE289AE12C988FF0004AB1A6 /* FavoriteProducts */, 4592A54924BF58DD00BC3DE0 /* ProductTagsViewController.swift */, 4592A54A24BF58DD00BC3DE0 /* ProductTagsViewController.xib */, 450C2CAF24CF006A00D570DD /* ProductTagsDataSource.swift */, @@ -13183,7 +13186,7 @@ EE289AE02C988FF0004AB1A6 /* FavoriteProductsUseCase.swift */, ); name = FavoriteProducts; - path = Classes/ViewRelated/Products/FavoriteProducts; + path = "Classes/ViewRelated/Products/Edit Product/FavoriteProducts"; sourceTree = SOURCE_ROOT; }; EE3272A229A88F670015F8D0 /* Onboarding */ = { @@ -17041,6 +17044,7 @@ B90DACC22A31BBC800365897 /* BarcodeSKUScannerProductFinderTests.swift in Sources */, D88D5A3B230B5D63007B6E01 /* MockAnalyticsProvider.swift in Sources */, CE5757AF2B7E7F7400AEEB6D /* AnalyticsHubCustomizeViewModelTests.swift in Sources */, + EEA3C2152CA3DB18000E82EC /* DefaultFavoriteProductsUseCaseTests.swift in Sources */, 6885E2CE2C32B2E2004C8D70 /* TotalsViewModelTests.swift in Sources */, EE1905822B50289100617C53 /* BlazeCampaignCreationFormViewModelTests.swift in Sources */, B9C4AB29280031AB007008B8 /* PaymentReceiptEmailParameterDeterminerTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/ProductSelectorViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/ProductSelectorViewModelTests.swift index 2105a2735bd..0af3e53fab4 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/ProductSelectorViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/ProductSelectorViewModelTests.swift @@ -1151,33 +1151,38 @@ final class ProductSelectorViewModelTests: XCTestCase { // Given let mockStorageManager = MockStorageManager() let mockStores = MockStoresManager(sessionManager: .testingInstance) + let favoriteProductsUseCase = MockFavoriteProductsUseCase() + favoriteProductsUseCase.favoriteProductIDsValue = [23, 89, 432] let viewModel = ProductSelectorViewModel( siteID: sampleSiteID, source: .orderForm(flow: .creation), storageManager: mockStorageManager, - stores: mockStores + stores: mockStores, + favoriteProductsUseCase: favoriteProductsUseCase ) var filteredStockStatus: ProductStockStatus? var filteredProductStatus: ProductStatus? var filteredProductType: ProductType? var filteredProductCategory: Yosemite.ProductCategory? + var filteredproductIDs: [Int64]? let filters = FilterProductListViewModel.Filters( stockStatus: .outOfStock, productStatus: .draft, promotableProductType: PromotableProductType(productType: .simple, isAvailable: true, promoteUrl: nil), productCategory: .init(categoryID: 123, siteID: sampleSiteID, parentID: 1, name: "Test", slug: "test"), - favoriteProduct: nil, + favoriteProduct: FavoriteProductsFilter(), numberOfActiveFilters: 1 ) mockStores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .synchronizeProducts(_, _, _, stockStatus, productStatus, productType, category, _, _, _, _, onCompletion): + case let .synchronizeProducts(_, _, _, stockStatus, productStatus, productType, category, _, productIDs, _, _, onCompletion): filteredStockStatus = stockStatus filteredProductType = productType filteredProductStatus = productStatus filteredProductCategory = category + filteredproductIDs = productIDs onCompletion(.success(true)) default: XCTFail("Received unsupported action: \(action)") @@ -1194,6 +1199,7 @@ final class ProductSelectorViewModelTests: XCTestCase { assertEqual(filteredProductType, filters.promotableProductType?.productType) assertEqual(filteredProductStatus, filters.productStatus) assertEqual(filteredProductCategory, filters.productCategory) + assertEqual(filteredproductIDs, favoriteProductsUseCase.favoriteProductIDsValue) } func test_searchProducts_are_triggered_with_correct_filters() async throws { @@ -1273,9 +1279,11 @@ final class ProductSelectorViewModelTests: XCTestCase { viewModel.updateFilters(filters) // Then - XCTAssertEqual(viewModel.productRows.count, 0) // no product matches the filter and search term - // When + waitUntil { + viewModel.productRows.count == 0 // no product matches the filter and search term + } + // When viewModel.searchTerm = "" waitUntil { viewModel.productRows.isNotEmpty @@ -1592,6 +1600,40 @@ final class ProductSelectorViewModelTests: XCTestCase { XCTAssertEqual(synchronizeProductsPages, [1, 1, 2]) XCTAssertEqual(searchProductsPages, [1]) } + + // MARK: - Favorite products + func test_it_loads_favorite_products_on_init() { + // Given + let favoriteProductsUseCase = MockFavoriteProductsUseCase() + let viewModel = ProductSelectorViewModel(siteID: sampleSiteID, + source: .orderForm(flow: .creation), + storageManager: storageManager, + stores: stores, + favoriteProductsUseCase: favoriteProductsUseCase) + // Then + waitUntil { + favoriteProductsUseCase.favoriteProductIDsCalled == true + } + } + + func test_it_loads_and_sets_favorite_product_IDs_correctly() async { + // Given + let favoriteProductsUseCase = MockFavoriteProductsUseCase() + let viewModel = ProductSelectorViewModel(siteID: sampleSiteID, + source: .orderForm(flow: .creation), + storageManager: storageManager, + stores: stores, + favoriteProductsUseCase: favoriteProductsUseCase) + XCTAssertTrue(viewModel.favoriteProductIDs.isEmpty) + + // When + let sampleProductIDs: [Int64] = [1, 2, 3] + favoriteProductsUseCase.favoriteProductIDsValue = sampleProductIDs + await viewModel.loadFavoriteProductIDs() + + // Then + XCTAssertEqual(viewModel.favoriteProductIDs, sampleProductIDs) + } } // MARK: - Utils diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/ProductListViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/ProductListViewModelTests.swift index 36e083d028d..d00a5ec72d4 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/ProductListViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/ProductListViewModelTests.swift @@ -340,4 +340,37 @@ final class ProductListViewModelTests: XCTestCase { // Then XCTAssertFalse(result) } + + // MARK: Favorite products + // + func test_it_loads_favorite_products_on_init() { + // Given + let favoriteProductsUseCase = MockFavoriteProductsUseCase() + let viewModel = ProductListViewModel(siteID: sampleSiteID, + stores: storesManager, + favoriteProductsUseCase: favoriteProductsUseCase) + + // Then + waitUntil { + favoriteProductsUseCase.favoriteProductIDsCalled == true + } + } + + func test_it_loads_and_sets_favorite_product_IDs_correctly() async { + // Given + let favoriteProductsUseCase = MockFavoriteProductsUseCase() + let viewModel = ProductListViewModel(siteID: sampleSiteID, + stores: storesManager, + favoriteProductsUseCase: favoriteProductsUseCase) + + XCTAssertTrue(viewModel.favoriteProductIDs.isEmpty) + + // When + let sampleProductIDs: [Int64] = [1, 2, 3] + favoriteProductsUseCase.favoriteProductIDsValue = sampleProductIDs + await viewModel.loadFavoriteProductIDs() + + // Then + XCTAssertEqual(viewModel.favoriteProductIDs, sampleProductIDs) + } }