From 1460855a329a4fecb5dc7e6282efa9af4a53b186 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 8 Sep 2025 13:45:06 +0300 Subject: [PATCH 1/7] Re-arrange split shipments availability logic --- .../WooShippingCreateLabelsView.swift | 4 +-- .../WooShippingCreateLabelsViewModel.swift | 33 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift index 2a73f947904..26abd650dbb 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift @@ -182,7 +182,7 @@ private extension WooShippingCreateLabelsView { .padding(.horizontal) } .accessibilityHint(Localization.Accessibility.editButtonHint) - .renderedIf(viewModel.hasUnfulfilledShipments) + .renderedIf(viewModel.editSplitShipmentsOptionVisible) } .disabled(viewModel.isPurchasingLabel) } @@ -191,7 +191,7 @@ private extension WooShippingCreateLabelsView { var mainView: some View { ScrollView { VStack(spacing: Layout.verticalSpacing) { - if viewModel.splitShipmentsAvailable { + if viewModel.splitShipmentsRowVisible { WooShippingSplitShipmentsRow(onShowingSplitShipments: { showingSplitShipments = true }) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index 2c4f19b240c..8ef76b6bcf6 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -80,12 +80,6 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { /// View model for split shipments. private(set) var splitShipmentsViewModel: WooShippingSplitShipmentsViewModel - var splitShipmentsAvailable: Bool { - itemsDataSource.items.map(\.quantity).reduce(0, +) > 1 && - shipments.count == 1 && - canViewLabel == false - } - private(set) var shipmentDetailViewModels: [WooShippingShipmentDetailsViewModel] = [] var currentShipmentDetailsViewModel: WooShippingShipmentDetailsViewModel { @@ -504,6 +498,33 @@ private extension WooShippingCreateLabelsViewModel { } } +// MARK: Split Shipments +// Accessors describing Split Shipments elements availability +extension WooShippingCreateLabelsViewModel { + private var hasMultipleProducts: Bool { + return itemsDataSource.items.map(\.quantity).reduce(0, +) > 1 + } + + private var splitShipmentsFeatureAvailable: Bool { + return true + } + + /// Determines if the "Edit split shipments" (pencil icon) is visible in top shipments bar. + var editSplitShipmentsOptionVisible: Bool { + splitShipmentsFeatureAvailable && + hasMultipleProducts && + hasUnfulfilledShipments + } + + /// Determines if the "Split Shipments" row is visible above the "Products" section + var splitShipmentsRowVisible: Bool { + splitShipmentsFeatureAvailable && + hasMultipleProducts && + shipments.count == 1 && + canViewLabel == false + } +} + // MARK: Utils private extension WooShippingCreateLabelsViewModel { From 36e53ba7c1374b439fd4d2e35d9f52a2d82620e8 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 8 Sep 2025 13:50:58 +0300 Subject: [PATCH 2/7] Conditionally disable split shipments feature for CIAB sites --- .../WooShippingCreateLabelsViewModel.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index 8ef76b6bcf6..002397b7cd7 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -235,6 +235,11 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { ) }() + + /// Provides checks for CIAB + /// Used to determine "Split Shipments" feature availability + private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol + /// Initialize the view model with or without an existing shipping label. init(order: Order, preselection: WooShippingCreateLabelSelection? = nil, @@ -243,6 +248,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { stores: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager, analytics: Analytics = ServiceLocator.analytics, + siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(), initialNoticeDelay: RunLoop.SchedulerTimeType.Stride = .seconds(2), onLabelPurchase: ((Bool) -> Void)? = nil) { self.order = order @@ -255,6 +261,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { self.stores = stores self.storageManager = storageManager self.analytics = analytics + self.siteCIABEligibilityChecker = siteCIABEligibilityChecker self.shippingSettingsService = shippingSettingsService self.weightUnit = shippingSettingsService.weightUnit ?? "" self.dimensionsUnit = shippingSettingsService.dimensionUnit ?? "" @@ -506,7 +513,7 @@ extension WooShippingCreateLabelsViewModel { } private var splitShipmentsFeatureAvailable: Bool { - return true + return siteCIABEligibilityChecker.isFeatureSupportedForCurrentSite(.splitShipments) } /// Determines if the "Edit split shipments" (pencil icon) is visible in top shipments bar. From dd4ca546e32dc6fd3f8833bf7151522add94664a Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 8 Sep 2025 18:30:28 +0300 Subject: [PATCH 3/7] Add tests for split shipments feature visibility WIP --- .../Classes/CIAB/CIABEligibilityChecker.swift | 9 -- .../CIAB/CIABEligibilityCheckerProtocol.swift | 11 ++ .../WooCommerce.xcodeproj/project.pbxproj | 8 ++ .../Mocks/MockCIABEligibilityChecker.swift | 31 +++++ ...ooShippingCreateLabelsViewModelTests.swift | 121 ++++++++++++++++++ 5 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift create mode 100644 WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift diff --git a/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift b/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift index 808d6469c78..102ca3cc023 100644 --- a/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift +++ b/WooCommerce/Classes/CIAB/CIABEligibilityChecker.swift @@ -3,15 +3,6 @@ import Foundation import Yosemite -protocol CIABEligibilityCheckerProtocol { - var isCurrentSiteCIAB: Bool { get } - - func isSiteCIAB(_ site: Site) -> Bool - - func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool - func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool -} - final class CIABEligibilityChecker { private let stores: StoresManager diff --git a/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift new file mode 100644 index 00000000000..54c82d7f844 --- /dev/null +++ b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift @@ -0,0 +1,11 @@ +import Foundation +import Yosemite + +protocol CIABEligibilityCheckerProtocol { + var isCurrentSiteCIAB: Bool { get } + + func isSiteCIAB(_ site: Site) -> Bool + + func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool + func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 3649e8afdc3..d2924ca63cb 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1245,6 +1245,8 @@ 2DB8916E2E27F7840001B175 /* OrderListCellViewModel+Localizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB8916A2E27F6CE0001B175 /* OrderListCellViewModel+Localizations.swift */; }; 2DCB54FA2E6AE8E100621F90 /* CIABEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */; }; 2DCB54FC2E6AFE6A00621F90 /* CIABAffectedFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */; }; + 2DE9DDFB2E6EF4A500155408 /* MockCIABEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */; }; + 2DE9DDFD2E6EF53C00155408 /* CIABEligibilityCheckerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */; }; 2DF0D1BC2E2907C100F8995C /* MarkOrderAsReadUseCase+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB88DA32E27DD790001B175 /* MarkOrderAsReadUseCase+Woo.swift */; }; 310D1B482734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */; }; 311237EE2714DA240033C44E /* CardPresentModalDisplayMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */; }; @@ -4434,6 +4436,8 @@ 2DB8916A2E27F6CE0001B175 /* OrderListCellViewModel+Localizations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderListCellViewModel+Localizations.swift"; sourceTree = ""; }; 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityChecker.swift; sourceTree = ""; }; 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABAffectedFeature.swift; sourceTree = ""; }; + 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCIABEligibilityChecker.swift; sourceTree = ""; }; + 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerProtocol.swift; sourceTree = ""; }; 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsLiveSiteInTestModeView.swift; sourceTree = ""; }; 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalDisplayMessage.swift; sourceTree = ""; }; 311D21E7264AEDB900102316 /* CardPresentModalScanningForReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningForReader.swift; sourceTree = ""; }; @@ -9058,6 +9062,7 @@ 2DCB54F82E6AE8C900621F90 /* CIAB */ = { isa = PBXGroup; children = ( + 2DE9DDFC2E6EF52E00155408 /* CIABEligibilityCheckerProtocol.swift */, 2DCB54FB2E6AFE6900621F90 /* CIABAffectedFeature.swift */, 2DCB54F92E6AE8D800621F90 /* CIABEligibilityChecker.swift */, ); @@ -10093,6 +10098,7 @@ 746791642108D853007CF1DC /* Mocks */ = { isa = PBXGroup; children = ( + 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */, 022900892E3019020028F6D7 /* MockPluginsService.swift */, 02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */, 02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */, @@ -16257,6 +16263,7 @@ B626C71B287659D60083820C /* CustomFieldsListView.swift in Sources */, 02ECD1E624FFB4E900735BE5 /* ProductFactory.swift in Sources */, 260520F42B87BA23005D5D59 /* WooAnalyticsEvent+ConnectivityTool.swift in Sources */, + 2DE9DDFD2E6EF53C00155408 /* CIABEligibilityCheckerProtocol.swift in Sources */, 579CDEFF274D7E7900E8903D /* StoreStatsUsageTracksEventEmitter.swift in Sources */, 203163AD2C1C5C54001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedNonRetryableAlertViewModel.swift in Sources */, CEDBDA472B6BEF2E002047D4 /* AnalyticsWebReport.swift in Sources */, @@ -17150,6 +17157,7 @@ EE8B421B2C04D18B0077C4E7 /* LastOrdersDashboardCardViewModelTests.swift in Sources */, 095A077E27CF486C007A61D2 /* ValueOneTableViewCellTests.swift in Sources */, E17E3BF9266917C10009D977 /* CardPresentModalScanningFailedTests.swift in Sources */, + 2DE9DDFB2E6EF4A500155408 /* MockCIABEligibilityChecker.swift in Sources */, DE66C56F2978F24200DAA978 /* ApplicationPasswordDisabledViewModelTests.swift in Sources */, EE1905942B62AE9100617C53 /* BlazeCreateCampaignIntroViewModelTests.swift in Sources */, 269098B627D2C09D001FEB07 /* ShippingInputTransformerTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift new file mode 100644 index 00000000000..059e4c0d37c --- /dev/null +++ b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift @@ -0,0 +1,31 @@ +import Foundation +import Yosemite +@testable import WooCommerce + +final class MockCIABEligibilityChecker: CIABEligibilityCheckerProtocol { + private let mockedIsCurrentSiteCIAB: Bool + private let mockedCIABSites: [Site] + private let mockedCIABDisabledFeatures: [CIABAffectedFeature] + + init(mockedIsCurrentSiteCIAB: Bool, mockedCIABSites: [Site] = [], mockedCIABDisabledFeatures: [CIABAffectedFeature] = CIABAffectedFeature.allCases) { + self.mockedIsCurrentSiteCIAB = mockedIsCurrentSiteCIAB + self.mockedCIABSites = mockedCIABSites + self.mockedCIABDisabledFeatures = mockedCIABDisabledFeatures + } + + var isCurrentSiteCIAB: Bool { + return mockedIsCurrentSiteCIAB + } + + func isSiteCIAB(_ site: Site) -> Bool { + return mockedCIABSites.contains(site) + } + + func isFeatureSupportedForCurrentSite(_ feature: CIABAffectedFeature) -> Bool { + return !mockedCIABDisabledFeatures.contains(feature) + } + + func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool { + return !mockedCIABDisabledFeatures.contains(feature) && mockedCIABSites.contains(site) + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift index 2549abd298c..479cb3b9d61 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift @@ -1038,6 +1038,95 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { XCTAssertTrue(originAddressesLoaded, "Origin addresses should be loaded from remote when missing") XCTAssertFalse(viewModel.originAddress.isEmpty, "Origin address should be set from loaded data") } + + // MARK: - split shipments feature visibility tests + + func test_splitShipmentsRowVisible_is_false_when_split_shipments_feature_is_unavailable() { + let stores = MockStoresManager(sessionManager: .testingInstance) + stores.whenReceivingAction(ofType: WooShippingAction.self) { action in + switch action { + case .verifyDestinationAddress(_, _, let completion): + completion(.success(WooShippingVerifyDestinationAddressSuccess(normalizedAddress: WooShippingNormalizedAddress.fake(), + isTrivialNormalization: nil, + isVerified: true))) + case .loadAccountSettings(_, let completion): + completion(.success(self.settings)) + case .loadOriginAddresses(_, let completion): + let originAddress = WooShippingOriginAddress.fake().copy(address1: "123 Main Street", defaultAddress: true) + completion(.success([originAddress])) + default: + XCTFail("Unexpected action: \(action)") + } + } + + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABDisabledFeatures: [.splitShipments] + ) + + // There exist 2 shipments, one of which has been fulfilled. + let product1 = Product.fake().copy(productID: 1) + let product2 = Product.fake().copy(productID: 2) + insert(product: product1) + insert(product: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) + } + + func test_splitShipmentsRowVisible_is_false_when_only_single_product_item_exists() { + } + + func test_splitShipmentsRowVisible_is_false_when_order_has_multiple_shipments() { + } + + func test_splitShipmentsRowVisible_is_true_when_requirements_met() { + } + + func test_editSplitShipmentsOptionVisible_is_false_when_split_shipments_feature_is_unavailable() { + } + + func test_editSplitShipmentsOptionVisible_is_false_when_only_single_product_item_exists() { + } + + func test_editSplitShipmentsOptionVisible_is_false_when_all_shipments_fulfilled() { + } + + func test_editSplitShipmentsOptionVisible_is_true_when_all_requirements_met() { + } } private extension WooShippingCreateLabelsViewModelTests { @@ -1057,6 +1146,25 @@ private extension WooShippingCreateLabelsViewModelTests { return mapGeneralSettings(from: "settings-general") } + func setupInitialDataLoadingMocks( + for stores: MockStoresManager, + originAddressesResult: Result<[WooShippingOriginAddress], Error> = .success([.fake().copy(id: "default", defaultAddress: true)]), + accountSettingsResult: Result = .success(.fake()) + ) { + stores.whenReceivingAction(ofType: WooShippingAction.self) { action in + switch action { + case .loadOriginAddresses(_, let completion): + completion(originAddressesResult) + case .loadAccountSettings(_, let completion): + completion(accountSettingsResult) + case .loadPackages, .verifyDestinationAddress: + break // Ignored for these tests + default: + XCTFail("Unexpected action: \(action)") + } + } + } + func insert(shipments: [WooShippingShipment], order: Order) { let storageOrder = storage.insertNewObject(ofType: StorageOrder.self) storageOrder.update(with: order) @@ -1091,10 +1199,23 @@ private extension WooShippingCreateLabelsViewModelTests { func insert(originAddress: WooShippingOriginAddress) { let storageAddress = storage.insertNewObject(ofType: StorageWooShippingOriginAddress.self) storageAddress.update(with: originAddress) + } func insert(accountSettings: ShippingLabelAccountSettings) { let storageSettings = storage.insertNewObject(ofType: StorageShippingLabelAccountSettings.self) storageSettings.update(with: accountSettings) } + + func insert(product: Product) { + let storageProduct = storage.insertNewObject(ofType: StorageProduct.self) + storageProduct.update(with: product) + } +} + +private extension WooShippingAccountSettings { + static func fake() -> WooShippingAccountSettings { + .init(storeOptions: .init(currencySymbol: "$", dimensionUnit: "in", weightUnit: "lbs", originCountry: "US"), + accountSettings: .fake()) + } } From 8982b59680495c82664e52c4fb8a5dc7f78befa3 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 9 Sep 2025 13:19:03 +0300 Subject: [PATCH 4/7] Update WooShippingCreateLabelsViewModel to use injected stores and storageManager in datasource --- .../WooShippingCreateLabelsViewModel.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index 002397b7cd7..c5d0b811b33 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -252,7 +252,11 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { initialNoticeDelay: RunLoop.SchedulerTimeType.Stride = .seconds(2), onLabelPurchase: ((Bool) -> Void)? = nil) { self.order = order - self.itemsDataSource = DefaultWooShippingItemsDataSource(order: order) + self.itemsDataSource = DefaultWooShippingItemsDataSource( + order: order, + storageManager: storageManager, + stores: stores + ) self.orderItems = WooShippingItemsViewModel(dataSource: itemsDataSource) self.onLabelPurchase = onLabelPurchase self.currencySettings = currencySettings From d826d228b579b87ab15f9d928d2857fb5d5bc962 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 9 Sep 2025 13:19:51 +0300 Subject: [PATCH 5/7] Implement tests for editSplitShipmentsOptionVisible and splitShipmentsRowVisible --- ...ooShippingCreateLabelsViewModelTests.swift | 420 +++++++++++++++++- 1 file changed, 405 insertions(+), 15 deletions(-) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift index 479cb3b9d61..d8d71411b3d 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift @@ -1041,14 +1041,19 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { // MARK: - split shipments feature visibility tests - func test_splitShipmentsRowVisible_is_false_when_split_shipments_feature_is_unavailable() { - let stores = MockStoresManager(sessionManager: .testingInstance) + private func initialConfigurationForSplitShipmentsTest(stores: MockStoresManager) { stores.whenReceivingAction(ofType: WooShippingAction.self) { action in switch action { case .verifyDestinationAddress(_, _, let completion): - completion(.success(WooShippingVerifyDestinationAddressSuccess(normalizedAddress: WooShippingNormalizedAddress.fake(), - isTrivialNormalization: nil, - isVerified: true))) + completion( + .success( + WooShippingVerifyDestinationAddressSuccess( + normalizedAddress: WooShippingNormalizedAddress.fake(), + isTrivialNormalization: nil, + isVerified: true + ) + ) + ) case .loadAccountSettings(_, let completion): completion(.success(self.settings)) case .loadOriginAddresses(_, let completion): @@ -1058,17 +1063,25 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { XCTFail("Unexpected action: \(action)") } } + } + + func test_splitShipmentsRowVisible_is_false_when_split_shipments_feature_is_unavailable() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + /// Is CIAB site with the `.splitShipments` disabled let mockSiteCIABChecker = MockCIABEligibilityChecker( mockedIsCurrentSiteCIAB: true, mockedCIABDisabledFeatures: [.splitShipments] ) - // There exist 2 shipments, one of which has been fulfilled. - let product1 = Product.fake().copy(productID: 1) - let product2 = Product.fake().copy(productID: 2) - insert(product: product1) - insert(product: product2) + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) let order = Order.fake().copy( siteID: siteID, @@ -1078,6 +1091,7 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { OrderItem.fake().copy(productID: product2.productID, quantity: 1) ]) + /// Single unfulfilled shipment let shipments = [ WooShippingShipment.fake().copy( siteID: siteID, @@ -1108,24 +1122,405 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { } func test_splitShipmentsRowVisible_is_false_when_only_single_product_item_exists() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Single product + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + storageManager.insertSampleProduct(readOnlyProduct: product1) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) } func test_splitShipmentsRowVisible_is_false_when_order_has_multiple_shipments() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Multiple shipments + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]), + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_1", + items: [ + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.splitShipmentsRowVisible) } func test_splitShipmentsRowVisible_is_true_when_requirements_met() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (feature available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertTrue(viewModel.splitShipmentsRowVisible) } func test_editSplitShipmentsOptionVisible_is_false_when_split_shipments_feature_is_unavailable() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Is CIAB site with the `.splitShipments` disabled + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABDisabledFeatures: [.splitShipments] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) } func test_editSplitShipmentsOptionVisible_is_false_when_only_single_product_item_exists() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Single product + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + storageManager.insertSampleProduct(readOnlyProduct: product1) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) } func test_editSplitShipmentsOptionVisible_is_false_when_all_shipments_fulfilled() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Each shipment is fulfilled + let shippingLabel1 = ShippingLabel.fake().copy(siteID: siteID, orderID: orderID, shippingLabelID: 134) + let shippingLabel2 = ShippingLabel.fake().copy(siteID: siteID, orderID: orderID, shippingLabelID: 245) + + /// 2 fulfilled shipments + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID) + ], + shippingLabel: shippingLabel1 + ), + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_1", + items: [ + .fake().copy(id: product2.productID) + ], + shippingLabel: shippingLabel2 + ) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertFalse(viewModel.editSplitShipmentsOptionVisible) } func test_editSplitShipmentsOptionVisible_is_true_when_all_requirements_met() { + // Given + let stores = MockStoresManager(sessionManager: .testingInstance) + initialConfigurationForSplitShipmentsTest(stores: stores) + + /// Non-CIAB site (split shipments available) + let mockSiteCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + mockedCIABDisabledFeatures: [] + ) + + /// Multiple products + let product1 = Product.fake().copy(siteID: siteID, productID: 1) + let product2 = Product.fake().copy(siteID: siteID, productID: 2) + + storageManager.insertSampleProduct(readOnlyProduct: product1) + storageManager.insertSampleProduct(readOnlyProduct: product2) + + let order = Order.fake().copy( + siteID: siteID, + orderID: orderID, + items: [ + OrderItem.fake().copy(productID: product1.productID, quantity: 1), + OrderItem.fake().copy(productID: product2.productID, quantity: 1) + ]) + + /// Single unfulfilled shipment + let shipments = [ + WooShippingShipment.fake().copy( + siteID: siteID, + orderID: orderID, + index: "shipment_0", + items: [ + .fake().copy(id: product1.productID), + .fake().copy(id: product2.productID) + ]) + ] + insert(shipments: shipments, order: order) + + // When + let viewModel = WooShippingCreateLabelsViewModel( + order: order, + stores: stores, + storageManager: storageManager, + siteCIABEligibilityChecker: mockSiteCIABChecker, + initialNoticeDelay: .seconds(0) + ) + + waitUntil { + viewModel.state == .ready + } + + // Then + XCTAssertTrue(viewModel.editSplitShipmentsOptionVisible) } } @@ -1206,11 +1601,6 @@ private extension WooShippingCreateLabelsViewModelTests { let storageSettings = storage.insertNewObject(ofType: StorageShippingLabelAccountSettings.self) storageSettings.update(with: accountSettings) } - - func insert(product: Product) { - let storageProduct = storage.insertNewObject(ofType: StorageProduct.self) - storageProduct.update(with: product) - } } private extension WooShippingAccountSettings { From 6aa37ea08c81c4117731ef74913a900773dc7bd5 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 9 Sep 2025 13:22:18 +0300 Subject: [PATCH 6/7] Delete extra line --- .../WooShippingCreateLabelsViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index c5d0b811b33..b4ddef0f290 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -235,7 +235,6 @@ final class WooShippingCreateLabelsViewModel: ObservableObject { ) }() - /// Provides checks for CIAB /// Used to determine "Split Shipments" feature availability private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol From 1882771f907e2d6799f01942db7a6c2720f7469c Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 9 Sep 2025 13:28:14 +0300 Subject: [PATCH 7/7] Silence periphery for yet unused protocol methods --- WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift index 54c82d7f844..6c7cd0aac92 100644 --- a/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift +++ b/WooCommerce/Classes/CIAB/CIABEligibilityCheckerProtocol.swift @@ -1,6 +1,7 @@ import Foundation import Yosemite +/// periphery: ignore - Will be used in upcoming changes for app feature gating protocol CIABEligibilityCheckerProtocol { var isCurrentSiteCIAB: Bool { get }