diff --git a/Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift b/Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift index 62e842d7dea..b67da2bce67 100644 --- a/Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift +++ b/Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift @@ -1,6 +1,7 @@ import Foundation import Observation import enum Yosemite.POSItem +import struct Yosemite.POSSimpleProduct import class Yosemite.PointOfSaleItemService import protocol Yosemite.PointOfSaleItemServiceProtocol import protocol Yosemite.PointOfSaleItemFetchStrategyFactoryProtocol diff --git a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index 85f92cfb6b0..b28606d9dcb 100644 --- a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -34,6 +34,7 @@ struct CardReaderConnectionStatusView: View { .padding(.horizontal, Constants.horizontalPadding) .frame(maxHeight: .infinity) } + .accessibilityIdentifier("pos-reader-connected") case .disconnecting: progressIndicatingCardReaderStatus(title: Localization.readerDisconnecting) case .cancellingConnection: @@ -55,6 +56,7 @@ struct CardReaderConnectionStatusView: View { ) .padding(Constants.disconnectedBorderInset) } + .accessibilityIdentifier("pos-connect-reader-button") } } .font(Constants.font) diff --git a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift index 3c13a8b851a..5e3b11496e5 100644 --- a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift +++ b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift @@ -24,6 +24,7 @@ struct PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView: View { } } .multilineTextAlignment(.center) + .accessibilityIdentifier("pos-card-payment-message") } } diff --git a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift index 680a6c8b7f3..c6261b7c921 100644 --- a/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift +++ b/Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift @@ -31,6 +31,7 @@ struct PointOfSalePaymentSuccessView: View { } } } + .accessibilityIdentifier("pos-payment-success-view") .onAppear { withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { isViewLoaded = true diff --git a/Modules/Sources/PointOfSale/Presentation/CartView.swift b/Modules/Sources/PointOfSale/Presentation/CartView.swift index 1c594cad998..49ad0e3cf3e 100644 --- a/Modules/Sources/PointOfSale/Presentation/CartView.swift +++ b/Modules/Sources/PointOfSale/Presentation/CartView.swift @@ -80,6 +80,7 @@ struct CartView: View { }) .background(backgroundColor.ignoresSafeArea(.all)) .accessibilityElement(children: .contain) + .accessibilityIdentifier("pos-cart-view") } } } @@ -175,6 +176,7 @@ private extension CartView { } .buttonStyle(POSFilledButtonStyle(size: .normal)) .disabled(CartViewHelper().hasUnresolvedItems(cart: posModel.cart)) + .accessibilityIdentifier("pos-checkout-button") } var backButtonConfiguration: POSPageHeaderBackButtonConfiguration? { diff --git a/Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift b/Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift index c3a08a3fd63..551cf0f56a2 100644 --- a/Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift +++ b/Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift @@ -156,6 +156,7 @@ struct ItemListRow: View { }, label: { SimpleProductCardView(product: product) }) + .accessibilityIdentifier("pos-product-card-\(product.productID)") case let .variableParentProduct(parentProduct): if #available(iOS 18.0, *) { NavigationLink(value: item) { diff --git a/Modules/Sources/PointOfSale/Presentation/POSFloatingControlView.swift b/Modules/Sources/PointOfSale/Presentation/POSFloatingControlView.swift index 7d78b023608..2923fa36928 100644 --- a/Modules/Sources/PointOfSale/Presentation/POSFloatingControlView.swift +++ b/Modules/Sources/PointOfSale/Presentation/POSFloatingControlView.swift @@ -40,6 +40,7 @@ struct POSFloatingControlView: View { } .frame(width: Constants.size) } + .accessibilityIdentifier("pos-menu-button") .background(backgroundColor) .cornerRadius(Constants.cornerRadius) .disabled(posModel.paymentState.card == .processingPayment) @@ -78,6 +79,7 @@ private extension POSFloatingControlView { icon: { Image(systemName: "rectangle.portrait.and.arrow.forward") } ) } + .accessibilityIdentifier("pos-exit-menu-item") Button { analytics.track(.pointOfSaleSettingsMenuItemTapped) showSettings = true diff --git a/Modules/Sources/PointOfSale/Presentation/PointOfSaleCollectCashView.swift b/Modules/Sources/PointOfSale/Presentation/PointOfSaleCollectCashView.swift index 3afaf199740..a45ce16125b 100644 --- a/Modules/Sources/PointOfSale/Presentation/PointOfSaleCollectCashView.swift +++ b/Modules/Sources/PointOfSale/Presentation/PointOfSaleCollectCashView.swift @@ -97,6 +97,7 @@ struct PointOfSaleCollectCashView: View { buttonFrame = $0 } .buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: isLoading)) + .accessibilityIdentifier("pos-mark-payment-complete-button") .frame(maxWidth: .infinity) .dynamicTypeSize(...DynamicTypeSize.accessibility1) .disabled(!isButtonEnabled) diff --git a/Modules/Sources/PointOfSale/Presentation/PointOfSaleDashboardView.swift b/Modules/Sources/PointOfSale/Presentation/PointOfSaleDashboardView.swift index 6e759542208..ec003f7db46 100644 --- a/Modules/Sources/PointOfSale/Presentation/PointOfSaleDashboardView.swift +++ b/Modules/Sources/PointOfSale/Presentation/PointOfSaleDashboardView.swift @@ -150,16 +150,13 @@ struct PointOfSaleDashboardView: View { } } .onChange(of: posModel.entryPointController.eligibilityState) { oldValue, newValue in - guard newValue == .eligible else { return } - Task { @MainActor in - await posModel.purchasableItemsController.loadItems(base: .root) - await posModel.couponsController.loadItems(base: .root) - await posModel.popularPurchasableItemsController.loadItems(base: .root) - } + guard case .eligible = newValue, oldValue != newValue else { return } + loadItemsWhenEligible() } .ignoresSafeArea(.keyboard) .onAppear { trackTimeForInitialLoadingState() + loadItemsWhenEligible() } .onChange(of: viewState) { oldValue, newValue in if newValue == .content && oldValue != newValue { @@ -249,6 +246,14 @@ private extension PointOfSaleDashboardView { self.waitingTimeTracker = nil } } + + func loadItemsWhenEligible() { + Task { @MainActor in + await posModel.purchasableItemsController.loadItems(base: .root) + await posModel.couponsController.loadItems(base: .root) + await posModel.popularPurchasableItemsController.loadItems(base: .root) + } + } } struct FloatingControlAreaSizeKey: EnvironmentKey { diff --git a/Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift b/Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift index 66aaaa45c56..9a63e5a0800 100644 --- a/Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift +++ b/Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift @@ -15,6 +15,7 @@ import class Yosemite.PointOfSaleItemService import protocol Yosemite.PointOfSaleSettingsServiceProtocol import struct Yosemite.SiteSetting import protocol Yosemite.PointOfSaleCouponFetchStrategyFactoryProtocol +import protocol Yosemite.PointOfSaleItemServiceProtocol /// periphery: ignore - public in preparation of move to POS module public struct PointOfSaleEntryPointView: View { @@ -67,9 +68,12 @@ public struct PointOfSaleEntryPointView: View { grdbManager: GRDBManagerProtocol?, catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?, isLocalCatalogEligible: Bool, - services: POSDependencyProviding) { + services: POSDependencyProviding, + itemProvider: PointOfSaleItemServiceProtocol? = nil) { self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange + let selectedItemProvider = itemProvider ?? PointOfSaleItemService(currencySettings: services.currency.currencySettings) + // Use observable controller with GRDB if local catalog is eligible, // otherwise fall back to standard controller. if isLocalCatalogEligible, let grdbManager = grdbManager, let catalogSyncCoordinator { @@ -81,13 +85,13 @@ public struct PointOfSaleEntryPointView: View { ) } else { self.itemsController = PointOfSaleItemsController( - itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings), + itemProvider: selectedItemProvider, itemFetchStrategyFactory: itemFetchStrategyFactory, analyticsProvider: services.analytics ) } self.purchasableItemsSearchController = PointOfSaleItemsController( - itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings), + itemProvider: selectedItemProvider, itemFetchStrategyFactory: itemFetchStrategyFactory, initialState: .init(containerState: .content, itemsStack: .init(root: .loaded([], hasMoreItems: true), itemStates: [:])), @@ -121,7 +125,7 @@ public struct PointOfSaleEntryPointView: View { self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker self.searchHistoryService = searchHistoryService self.popularPurchasableItemsController = PointOfSaleItemsController( - itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings), + itemProvider: selectedItemProvider, itemFetchStrategyFactory: popularItemFetchStrategyFactory, analyticsProvider: services.analytics ) diff --git a/Modules/Sources/PointOfSale/Presentation/PointOfSaleExitPosAlertView.swift b/Modules/Sources/PointOfSale/Presentation/PointOfSaleExitPosAlertView.swift index 3ab43da9a22..73ee5fd392f 100644 --- a/Modules/Sources/PointOfSale/Presentation/PointOfSaleExitPosAlertView.swift +++ b/Modules/Sources/PointOfSale/Presentation/PointOfSaleExitPosAlertView.swift @@ -19,6 +19,7 @@ struct PointOfSaleExitPosAlertView: View { Text(Image(systemName: "xmark")) .font(.posButtonSymbolLarge) } + .accessibilityIdentifier("pos-exit-modal-close-button") .foregroundColor(Color.posOnSurfaceVariantLowest) } Text(Localization.exitTitle) @@ -33,6 +34,7 @@ struct PointOfSaleExitPosAlertView: View { } label: { Text(Localization.exitButton) } + .accessibilityIdentifier("pos-exit-confirm-button") .buttonStyle(POSFilledButtonStyle(size: .normal)) } .padding(Constants.padding) diff --git a/Modules/Sources/PointOfSale/Presentation/TotalsView.swift b/Modules/Sources/PointOfSale/Presentation/TotalsView.swift index 43f9118d182..3e7c4b4ec5b 100644 --- a/Modules/Sources/PointOfSale/Presentation/TotalsView.swift +++ b/Modules/Sources/PointOfSale/Presentation/TotalsView.swift @@ -67,6 +67,7 @@ struct TotalsView: View { case .error(.other(let message), let handler): PointOfSaleOrderSyncErrorMessageView(message: message, retryHandler: handler) .transition(.opacity) + case .error(.invalidCoupon(let message), let handler): PointOfSaleOrderSyncCouponsErrorMessageView(message: message, retryHandler: handler) .transition(.opacity) @@ -411,6 +412,7 @@ private struct TotalFieldView: View { } .accessibilityElement(children: .combine) .accessibilityAddTraits(.isHeader) + .accessibilityIdentifier("pos-total-field") .foregroundColor(Color.posOnSurface) } } @@ -508,7 +510,7 @@ private struct CardPaymentView: View { var body: some View { if viewHelper.shouldShowDisconnectedMessage(readerConnectionStatus: cardReaderConnectionStatus, - paymentState: paymentState) { + paymentState: paymentState) { PointOfSaleCardPresentPaymentReaderDisconnectedMessageView { connectCardReaderAction() } @@ -547,6 +549,7 @@ private struct CashPaymentButton: View { .layoutPriority(1) .dynamicTypeSize(...DynamicTypeSize.accessibility1) .buttonStyle(POSOutlinedButtonStyle(size: .normal)) + .accessibilityIdentifier("pos-cash-payment-button") .padding(.horizontal, TotalsView.Constants.buttonHorizontalPadding) .safeAreaPadding(.bottom, TotalsView.Constants.cashButtonBottomPadding) .renderedIf(viewHelper.shouldShowCollectCashPaymentButton( diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/GetStartedScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/GetStartedScreen.swift index bf89b3453d4..abf672a9b5d 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/GetStartedScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/GetStartedScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/HelpScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/HelpScreen.swift index 7d4f2a63041..40b5a794742 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/HelpScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/HelpScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift index 896cd025c45..f569f983bee 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LinkOrPasswordScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift index 2b930455913..66220119e49 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginCheckMagicLinkScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginEmailScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginEmailScreen.swift index 7186cdfc5a9..009e08746c4 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginEmailScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginEmailScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift index d39d5b80526..d1b7f72bfb4 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginEpilogueScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginOnboardingScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginOnboardingScreen.swift index 3f7beb20dbf..7cf710aaec0 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginOnboardingScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginOnboardingScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift index 131cfd4284c..ff68c3d468f 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginPasswordScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest import XCUITestHelpers diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift index 336505b5a50..f1e2a75a191 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginSiteAddressScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift index 091246bf585..ee664bb6e9d 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/LoginUsernamePasswordScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/PasswordScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/PasswordScreen.swift index 0ef9f853d07..a910fe03438 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/PasswordScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/PasswordScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/PrologueScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/PrologueScreen.swift index 427c09359ac..2cfbed2c252 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/PrologueScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/PrologueScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Login/TwoFAScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Login/TwoFAScreen.swift index a2eafa01bc8..2d63ebde221 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Login/TwoFAScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Login/TwoFAScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Menu/MenuScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Menu/MenuScreen.swift index 7967a69f721..1c83167274e 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Menu/MenuScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Menu/MenuScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/MyStore/MyStoreScreen.swift b/Modules/Sources/UITestsFoundation/Screens/MyStore/MyStoreScreen.swift index 98c8b5551c9..b8d87487c11 100644 --- a/Modules/Sources/UITestsFoundation/Screens/MyStore/MyStoreScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/MyStore/MyStoreScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomAmountScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomAmountScreen.swift index f39244bf747..f6002368a27 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomAmountScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomAmountScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomerDetailsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomerDetailsScreen.swift index e0f69c8bf1e..d003f997611 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomerDetailsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/AddCustomerDetailsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/AddProductScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/AddProductScreen.swift index 510fb6d419b..38a98fb2ec3 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/AddProductScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/AddProductScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/AddShippingScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/AddShippingScreen.swift index 15538e333a4..c2d2a2f2eae 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/AddShippingScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/AddShippingScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/CardPresentPaymentsModalScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/CardPresentPaymentsModalScreen.swift index 402e92aef2e..f7aa49eb56a 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/CardPresentPaymentsModalScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/CardPresentPaymentsModalScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerDetailsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerDetailsScreen.swift index f2b98c26cde..d2a02ef309d 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerDetailsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerDetailsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerNoteScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerNoteScreen.swift index ee0279e20de..0b65f0f396d 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerNoteScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/CustomerNoteScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/OrderSearchScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/OrderSearchScreen.swift index deca72ebb3c..957b16f731e 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/OrderSearchScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/OrderSearchScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/OrderStatusScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/OrderStatusScreen.swift index 4b720126cb6..ff2926a4896 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/OrderStatusScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/OrderStatusScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/OrdersScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/OrdersScreen.swift index 8fe9701a510..3718504b651 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/OrdersScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/OrdersScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/PaymentMethodsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/PaymentMethodsScreen.swift index f8579ff7728..705f36c57aa 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/PaymentMethodsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/PaymentMethodsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/SingleOrderScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/SingleOrderScreen.swift index 124999a2d29..1b8f59eb905 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/SingleOrderScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/SingleOrderScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest @@ -82,6 +83,7 @@ public final class SingleOrderScreen: ScreenObject { return try PaymentMethodsScreen() } + @discardableResult public func goBackToOrdersScreen() throws -> OrdersScreen { let orderDetailTableView = app.tables["order-details-table-view"] diff --git a/Modules/Sources/UITestsFoundation/Screens/Orders/UnifiedOrderScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Orders/UnifiedOrderScreen.swift index baa86a1373d..509c5254c82 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Orders/UnifiedOrderScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Orders/UnifiedOrderScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/POS/POSScreen.swift b/Modules/Sources/UITestsFoundation/Screens/POS/POSScreen.swift new file mode 100644 index 00000000000..46466e605f2 --- /dev/null +++ b/Modules/Sources/UITestsFoundation/Screens/POS/POSScreen.swift @@ -0,0 +1,160 @@ +import ScreenObject +import XCTest +// periphery: ignore - used for UI testing +public final class POSScreen: ScreenObject { + private let cartViewGetter: (XCUIApplication) -> XCUIElement = { + $0.otherElements["pos-cart-view"] + } + + public init(app: XCUIApplication = XCUIApplication()) throws { + try super.init( + expectedElementGetters: [cartViewGetter], + app: app + ) + } + + @discardableResult + public func tapAddProduct(productID: Int) -> Self { + let productButton = app.buttons["pos-product-card-\(productID)"] + + guard productButton.waitForExistence(timeout: 1) else { + return self + } + productButton.tap() + + return self + } + + @discardableResult + public func tapCheckout() -> Self { + let checkoutButton = app.buttons["pos-checkout-button"] + + guard checkoutButton.waitForExistence(timeout: 1) else { + return self + } + + checkoutButton.tap() + return self + } + + @discardableResult + public func waitForTotalsLoaded() -> Self { + // Wait for the actual totals to load (not shimmer/ghost state) + // This waits for orderState to be .loaded and payment to start + let totalField = app.otherElements["pos-total-field"] + + guard totalField.waitForExistence(timeout: 3) else { + return self + } + + return self + } + + @discardableResult + public func waitForCardPaymentReady() -> Self { + // Wait for card payment UI to be ready with "Tap, swipe or insert card" message + let cardPaymentMessage = app.otherElements["pos-card-payment-message"] + + guard cardPaymentMessage.waitForExistence(timeout: 3) else { + return self + } + + return self + } + + @discardableResult + public func tapConnectReader() -> Self { + // Tap the "Connect your reader" button to initiate card reader connection + let connectButton = app.buttons["pos-connect-reader-button"] + + guard connectButton.waitForExistence(timeout: 3) else { + return self + } + + connectButton.tap() + return self + } + + @discardableResult + public func waitForReaderConnected() -> Self { + // Wait for the reader connection status to show "Reader connected" + let connectedStatus = app.otherElements["pos-reader-connected"] + + guard connectedStatus.waitForExistence(timeout: 3) else { + return self + } + + return self + } + + @discardableResult + public func tapCashPayment() -> Self { + let cashButton = app.buttons["pos-cash-payment-button"] + + guard cashButton.waitForExistence(timeout: 3) else { + return self + } + + cashButton.tap() + return self + } + + @discardableResult + public func tapMarkPaymentComplete() -> Self { + let completeButton = app.buttons["pos-mark-payment-complete-button"] + + guard completeButton.waitForExistence(timeout: 3) else { + return self + } + + completeButton.tap() + return self + } + + @discardableResult + public func waitForPaymentSuccess() -> Self { + let successView = app.otherElements["pos-payment-success-view"] + + guard successView.waitForExistence(timeout: 3) else { + return self + } + + return self + } + + @discardableResult + public func tapMenuButton() -> Self { + let menuButton = app.buttons["pos-menu-button"] + + guard menuButton.waitForExistence(timeout: 3) else { + return self + } + + menuButton.tap() + return self + } + + @discardableResult + public func tapExitMenuItem() -> Self { + let exitMenuItem = app.buttons["pos-exit-menu-item"] + + guard exitMenuItem.waitForExistence(timeout: 3) else { + return self + } + + exitMenuItem.tap() + return self + } + + @discardableResult + public func confirmExitPOS() throws -> TabNavComponent { + let exitButton = app.buttons["pos-exit-confirm-button"] + + guard exitButton.waitForExistence(timeout: 3) else { + return try TabNavComponent() + } + + exitButton.tap() + return try TabNavComponent() + } +} diff --git a/Modules/Sources/UITestsFoundation/Screens/Payments/CardReaderManualsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Payments/CardReaderManualsScreen.swift index 12329a1c5d1..9ae38228d58 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Payments/CardReaderManualsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Payments/CardReaderManualsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Payments/PaymentsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Payments/PaymentsScreen.swift index fbb9cf0798c..f1cf291861a 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Payments/PaymentsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Payments/PaymentsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Products/ProductFilterScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Products/ProductFilterScreen.swift index a81f4ebb38d..cb19c02d5c4 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Products/ProductFilterScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Products/ProductFilterScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Products/ProductSearchScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Products/ProductSearchScreen.swift index 3e6fb1a6624..2975f61f9bb 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Products/ProductSearchScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Products/ProductSearchScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Products/ProductsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Products/ProductsScreen.swift index aa3a7d30f59..e5f3c6dd2df 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Products/ProductsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Products/ProductsScreen.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Products/SingleProductScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Products/SingleProductScreen.swift index 515dc3b0bc8..df932beb228 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Products/SingleProductScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Products/SingleProductScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest import XCUITestHelpers diff --git a/Modules/Sources/UITestsFoundation/Screens/Reviews/ReviewsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Reviews/ReviewsScreen.swift index 3b4727b1607..c5c860e2be0 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Reviews/ReviewsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Reviews/ReviewsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Reviews/SingleReviewScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Reviews/SingleReviewScreen.swift index 30e20601438..e224c4a3b4a 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Reviews/SingleReviewScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Reviews/SingleReviewScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Settings/BetaFeaturesScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Settings/BetaFeaturesScreen.swift index 2ded43b573c..12c5054523d 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Settings/BetaFeaturesScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Settings/BetaFeaturesScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/Settings/SettingsScreen.swift b/Modules/Sources/UITestsFoundation/Screens/Settings/SettingsScreen.swift index cc42547d8c1..7446e5f65f9 100644 --- a/Modules/Sources/UITestsFoundation/Screens/Settings/SettingsScreen.swift +++ b/Modules/Sources/UITestsFoundation/Screens/Settings/SettingsScreen.swift @@ -1,3 +1,4 @@ +//periphery:ignore:all import ScreenObject import XCTest diff --git a/Modules/Sources/UITestsFoundation/Screens/TabNavComponent.swift b/Modules/Sources/UITestsFoundation/Screens/TabNavComponent.swift index 833c156a0a3..48d21f4e9ed 100644 --- a/Modules/Sources/UITestsFoundation/Screens/TabNavComponent.swift +++ b/Modules/Sources/UITestsFoundation/Screens/TabNavComponent.swift @@ -2,27 +2,36 @@ import ScreenObject import XCTest public final class TabNavComponent: ScreenObject { - + // periphery:ignore private let myStoreTabButtonGetter: (XCUIApplication) -> XCUIElement = { $0.tabBars.firstMatch.buttons["tab-bar-my-store-item"] } - + // periphery:ignore private let ordersTabButtonGetter: (XCUIApplication) -> XCUIElement = { $0.tabBars.firstMatch.buttons["tab-bar-orders-item"] } - + // periphery:ignore private let productsTabButtonGetter: (XCUIApplication) -> XCUIElement = { $0.tabBars.firstMatch.buttons["tab-bar-products-item"] } - + // periphery:ignore + private let posTabButtonGetter: (XCUIApplication) -> XCUIElement = { + $0.tabBars.firstMatch.buttons["tab-bar-pos-item"] + } + // periphery:ignore private let menuTabButtonGetter: (XCUIApplication) -> XCUIElement = { $0.tabBars.firstMatch.buttons["tab-bar-menu-item"] } - + // periphery:ignore private var myStoreTabButton: XCUIElement { myStoreTabButtonGetter(app) } + // periphery:ignore private var ordersTabButton: XCUIElement { ordersTabButtonGetter(app) } + // periphery:ignore private var menuTabButton: XCUIElement { menuTabButtonGetter(app) } + // periphery:ignore private var productsTabButton: XCUIElement { productsTabButtonGetter(app) } + // periphery:ignore + private var posTabButton: XCUIElement { posTabButtonGetter(app) } public init(app: XCUIApplication = XCUIApplication()) throws { try super.init( @@ -35,34 +44,46 @@ public final class TabNavComponent: ScreenObject { ) } + // periphery:ignore @discardableResult public func goToMyStoreScreen() throws -> MyStoreScreen { myStoreTabButton.tap() return try MyStoreScreen() } + // periphery:ignore @discardableResult public func goToOrdersScreen() throws -> OrdersScreen { ordersTabButton.tap() return try OrdersScreen() } + // periphery:ignore @discardableResult public func goToProductsScreen() throws -> ProductsScreen { productsTabButton.tap() return try ProductsScreen() } + // periphery:ignore + public func goToPOSScreen() throws -> POSScreen { + posTabButton.tap() + return try POSScreen() + } + + // periphery:ignore @discardableResult public func goToMenuScreen() throws -> MenuScreen { menuTabButton.tap() return try MenuScreen() } + // periphery:ignore static func isLoaded() -> Bool { (try? TabNavComponent().isLoaded) ?? false } + // periphery:ignore // TODO: This paradigm is used enough around the test suits that it would be worth extracting to `ScreenObject`. static func isVisible() -> Bool { guard let tabNavComponent = try? TabNavComponent() else { return false } diff --git a/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockAppSettingsActionHandler.swift b/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockAppSettingsActionHandler.swift index b0097a3e6a2..bdf3930493c 100644 --- a/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockAppSettingsActionHandler.swift +++ b/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockAppSettingsActionHandler.swift @@ -61,7 +61,9 @@ struct MockAppSettingsActionHandler: MockActionHandler { .getAppPasswordsExperimentSettingState, .getPOSSurveyCurrentMerchantNotificationScheduled, .getPOSSurveyPotentialMerchantNotificationScheduled, - .getHasPOSBeenOpenedAtLeastOnce: + .getHasPOSBeenOpenedAtLeastOnce, + .setHasPOSBeenOpenedAtLeastOnce, + .setPOSLastOpenedDate: break default: unimplementedAction(action: action) } diff --git a/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockCardPresentPaymentActionHandler.swift b/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockCardPresentPaymentActionHandler.swift index b9caf7fff10..9287d57a300 100644 --- a/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockCardPresentPaymentActionHandler.swift +++ b/Modules/Sources/Yosemite/Model/Mocks/ActionHandlers/MockCardPresentPaymentActionHandler.swift @@ -21,6 +21,8 @@ struct MockCardPresentPaymentActionHandler: MockActionHandler { onCompletion(true) case .observeConnectedReaders(let onCompletion): observeConnectedReaders(onCompletion: onCompletion) + case .observeCardReaderUpdateState(let onCompletion): + observeCardReaderUpdateState(onCompletion: onCompletion) case .collectPayment(_, _, _, let onCardReaderMessage, _, _): // This immediately brings up the `CardPresentModalTapCard` screen, which is used by // `WooCommerceScreenshots` to display it for screenshotting purpose. @@ -51,4 +53,8 @@ struct MockCardPresentPaymentActionHandler: MockActionHandler { let cardReaders = objectGraph.cardReaders onCompletion(cardReaders) } + + private func observeCardReaderUpdateState(onCompletion: @escaping (AnyPublisher) -> Void) { + onCompletion(Just(.none).eraseToAnyPublisher()) + } } diff --git a/WooCommerce/Classes/POS/Mocks/CardPresentPaymentServiceScreenshotMock.swift b/WooCommerce/Classes/POS/Mocks/CardPresentPaymentServiceScreenshotMock.swift new file mode 100644 index 00000000000..9a7b948eeb7 --- /dev/null +++ b/WooCommerce/Classes/POS/Mocks/CardPresentPaymentServiceScreenshotMock.swift @@ -0,0 +1,78 @@ +import Foundation +import Yosemite +import Combine +import PointOfSale + +final class CardPresentPaymentServiceScreenshotMock: CardPresentPaymentFacade { + let paymentEventPublisher: AnyPublisher + let readerConnectionStatusPublisher: AnyPublisher + let cardReaderUpdateStatePublisher: AnyPublisher + + private let paymentEventSubject = PassthroughSubject() + + init() { + paymentEventPublisher = paymentEventSubject + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + + // Always return a connected card reader for screenshots + let mockReader = CardPresentPaymentCardReader( + name: "Simulated POS E", + batteryLevel: 0.5, + softwareVersion: "1.00.03.34-SZZZ_Generic_v45-300001" + ) + readerConnectionStatusPublisher = Just(.connected(mockReader)) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + + // No updates needed for screenshots + cardReaderUpdateStatePublisher = Just(.none) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + func connectReader(using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentReaderConnectionResult { + // Return connected reader immediately + let mockReader = CardPresentPaymentCardReader( + name: "Simulated POS E", + batteryLevel: 0.5, + softwareVersion: "1.00.03.34-SZZZ_Generic_v45-300001" + ) + return .connected(mockReader) + } + + func disconnectReader() async { + // No-op for screenshots + } + + func updateCardReaderSoftware() async throws { + // No-op for screenshots + } + + func collectPayment(for order: Order, using connectionMethod: CardReaderConnectionMethod, channel: PaymentChannel) async throws -> CardPresentPaymentResult { + + // 1. Validating order + paymentEventSubject.send(.show(eventDetails: .validatingOrder(cancelPayment: {}))) + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + // 2. Preparing reader + paymentEventSubject.send(.show(eventDetails: .preparingForPayment(cancelPayment: {}))) + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + // 3. Ready to accept card + let inputMethods: Yosemite.CardReaderInput = [.tap, .swipe, .insert] + paymentEventSubject.send(.show(eventDetails: .tapSwipeOrInsertCard(inputMethods: inputMethods, cancelPayment: {}))) + + try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds, give it some time for the screenshot + + return .success(CardPresentPaymentTransaction()) + } + + func cancelPayment() { + // No-op for screenshots + } + + func cancelPayment() async throws { + // No-op for screenshots + } +} diff --git a/WooCommerce/Classes/POS/Mocks/POSOrderServiceScreenshotMock.swift b/WooCommerce/Classes/POS/Mocks/POSOrderServiceScreenshotMock.swift new file mode 100644 index 00000000000..5e876da3d72 --- /dev/null +++ b/WooCommerce/Classes/POS/Mocks/POSOrderServiceScreenshotMock.swift @@ -0,0 +1,111 @@ +import Foundation +import Yosemite +import WooFoundationCore +import class WooFoundation.CurrencySettings +import struct NetworkingCore.OrderItem +import PointOfSale + +/// Mock order service for screenshot tests that returns immediate loaded state +final class POSOrderServiceScreenshotMock: POSOrderServiceProtocol { + // periphery: ignore - needed for conformance, not explicitely for the mock + private let currency: String + + init(currency: String) { + self.currency = currency + } + + func syncOrder(cart: POSCart, currency: CurrencyCode) async throws -> Order { + // Create a mock order with totals calculated from the cart + // For screenshot tests with 2 products: $35.00 + $45.00 = $80.00 + let orderItems = [ + OrderItem( + itemID: 1, + name: "Product 1", + productID: 1, + variationID: 0, + quantity: 1, + price: NSDecimalNumber(string: "35.00"), + sku: nil, + subtotal: "35.00", + subtotalTax: "0.00", + taxClass: "", + taxes: [], + total: "35.00", + totalTax: "0.00", + attributes: [], + addOns: [], + image: nil, + parent: nil, + bundleConfiguration: [] + ), + OrderItem( + itemID: 2, + name: "Product 2", + productID: 2, + variationID: 0, + quantity: 1, + price: NSDecimalNumber(string: "45.00"), + sku: nil, + subtotal: "45.00", + subtotalTax: "0.00", + taxClass: "", + taxes: [], + total: "45.00", + totalTax: "0.00", + attributes: [], + addOns: [], + image: nil, + parent: nil, + bundleConfiguration: [] + ) + ] + + let total = "80.00" + let tax = "0.00" + + return Order(siteID: 0, + orderID: 1, + parentID: 0, + customerID: 0, + orderKey: "", + isEditable: true, + needsPayment: true, + needsProcessing: true, + number: "1", + status: .pending, + currency: currency.rawValue, + currencySymbol: "$", + customerNote: nil, + dateCreated: Date(), + dateModified: Date(), + datePaid: nil, + discountTotal: "0.00", + discountTax: "0.00", + shippingTotal: "0.00", + shippingTax: "0.00", + total: total, + totalTax: tax, + paymentMethodID: "", + paymentMethodTitle: "", + paymentURL: nil, + chargeID: nil, + items: orderItems, // Necessary for the card payment flow to be presented in POS + billingAddress: nil, + shippingAddress: nil, + shippingLines: [], + coupons: [], + refunds: [], + fees: [], + taxes: [], + customFields: [], + renewalSubscriptionID: nil, + appliedGiftCards: [], + attributionInfo: nil, + shippingLabels: [], + createdVia: "pos") + } + + func updatePOSOrder(orderID: Int64, recipientEmail: String) async throws {} + + func markOrderAsCompletedWithCashPayment(order: Order, changeDueAmount: String?) async throws {} +} diff --git a/WooCommerce/Classes/POS/Mocks/PointOfSaleItemServiceScreenshotMock.swift b/WooCommerce/Classes/POS/Mocks/PointOfSaleItemServiceScreenshotMock.swift new file mode 100644 index 00000000000..0c855690292 --- /dev/null +++ b/WooCommerce/Classes/POS/Mocks/PointOfSaleItemServiceScreenshotMock.swift @@ -0,0 +1,79 @@ +import Foundation +import Yosemite + +final class PointOfSaleItemServiceScreenshotMock: Yosemite.PointOfSaleItemServiceProtocol { + + func providePointOfSaleItems(pageNumber: Int, + fetchStrategy: Yosemite.PointOfSalePurchasableItemFetchStrategy) async throws -> PagedItems { + let port = UserDefaults.standard.integer(forKey: "mocks-port") + let mockResourceUrlHost = "http://localhost:\(port)/" + + let mockItems = Self.makeScreenshotMockItems(mockResourceUrlHost: mockResourceUrlHost) + + return PagedItems(items: mockItems, hasMorePages: false, totalItems: mockItems.count) + } + + func providePointOfSaleVariationItems(for parentProduct: Yosemite.POSVariableParentProduct, + pageNumber: Int, + fetchStrategy: Yosemite.PointOfSalePurchasableItemFetchStrategy) async throws -> PagedItems { + // Not needed for screenshot tests, return empty + return PagedItems(items: [], hasMorePages: false, totalItems: 0) + } + + private static func makeScreenshotMockItems(mockResourceUrlHost: String) -> [Yosemite.POSItem] { + let product1 = Yosemite.POSSimpleProduct( + id: UUID(), + name: "Rose Gold Shades", + formattedPrice: "$35.00", + productImageSource: mockResourceUrlHost + "rose-gold-shades", + productID: 1, + price: "35.00", + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock" + ) + + let product2 = Yosemite.POSSimpleProduct( + id: UUID(), + name: "Black Coral Shades", + formattedPrice: "$45.00", + productImageSource: mockResourceUrlHost + "black-coral-shades", + productID: 2, + price: "45.00", + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock" + ) + + let product3 = Yosemite.POSSimpleProduct( + id: UUID(), + name: "Akoya Pearl Shades", + formattedPrice: "$50.00", + productImageSource: mockResourceUrlHost + "akoya-pearl-shades", + productID: 3, + price: "50.00", + manageStock: true, + stockQuantity: 10, + stockStatusKey: "instock" + ) + + let product4 = Yosemite.POSSimpleProduct( + id: UUID(), + name: "Malaya Shades", + formattedPrice: "$40.00", + productImageSource: mockResourceUrlHost + "malaya-shades", + productID: 4, + price: "40.00", + manageStock: false, + stockQuantity: nil, + stockStatusKey: "instock" + ) + + return [ + .simpleProduct(product1), + .simpleProduct(product2), + .simpleProduct(product3), + .simpleProduct(product4) + ] + } +} diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 481cff9b1fd..809ed604240 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -2,12 +2,13 @@ import Foundation import UIKit import SwiftUI import Yosemite +import Combine import class WooFoundation.CurrencySettings +import WooFoundationCore import protocol Storage.GRDBManagerProtocol import protocol Storage.StorageManagerType -import class WooFoundationCore.CurrencyFormatter import struct NetworkingCore.JetpackSite -import struct Combine.AnyPublisher +import struct NetworkingCore.OrderItem import PointOfSale protocol POSTabVisibilityCheckerProtocol { @@ -198,9 +199,15 @@ private extension POSTabCoordinator { let serviceAdaptor = POSServiceLocatorAdaptor() let collectPaymentAnalyticsAdaptor = POSCollectOrderPaymentAnalyticsAdaptor(analytics: serviceAdaptor.analytics) - let cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID, + + let cardPresentPaymentService: CardPresentPaymentFacade + if ProcessConfiguration.shouldUseMockCardPresentPayment { + cardPresentPaymentService = CardPresentPaymentServiceScreenshotMock() + } else { + cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID, stores: storesManager, collectOrderPaymentAnalyticsTracker: collectPaymentAnalyticsAdaptor) + } let settingsService = PointOfSaleSettingsService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, @@ -222,11 +229,26 @@ private extension POSTabCoordinator { if let receiptService = POSReceiptService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported), - let orderService = POSOrderService(siteID: siteID, - credentials: credentials, - selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported) { + appPasswordSupportState: isAppPasswordSupported) { + + let orderService: POSOrderServiceProtocol + if ProcessConfiguration.shouldBypassPOSOrderSyncing { + orderService = POSOrderServiceScreenshotMock(currency: currencySettings.currencyCode.rawValue) + } else if let posOrderService = POSOrderService(siteID: siteID, + credentials: credentials, + selectedSite: defaultSitePublisher, + appPasswordSupportState: isAppPasswordSupported) { + orderService = posOrderService + } else { + DDLogError("POSOrderService not provided") + return + } + + var itemProvider: Yosemite.PointOfSaleItemServiceProtocol? = nil + if ProcessConfiguration.shouldLoadMockedPOSProducts { + itemProvider = PointOfSaleItemServiceScreenshotMock() + } + let posView = PointOfSaleEntryPointView( siteID: siteID, itemFetchStrategyFactory: posItemFetchStrategyFactory, @@ -259,7 +281,8 @@ private extension POSTabCoordinator { grdbManager: grdbManager, catalogSyncCoordinator: catalogSyncCoordinator, isLocalCatalogEligible: isLocalCatalogEligible, - services: serviceAdaptor + services: serviceAdaptor, + itemProvider: itemProvider ) let hostingController = UIHostingController(rootView: posView) diff --git a/WooCommerce/Classes/System/ProcessConfiguration.swift b/WooCommerce/Classes/System/ProcessConfiguration.swift index 96d089c5e64..bdf2991e4ee 100644 --- a/WooCommerce/Classes/System/ProcessConfiguration.swift +++ b/WooCommerce/Classes/System/ProcessConfiguration.swift @@ -21,4 +21,24 @@ struct ProcessConfiguration { static var shouldSimulatePushNotification: Bool { ProcessInfo.processInfo.arguments.contains("-mocks-push-notification") } + + /// Returns `true` when POS eligibility checks should be bypassed for screenshot tests. + static var shouldBypassPOSEligibilityChecks: Bool { + ProcessInfo.processInfo.arguments.contains("bypass-pos-eligibility-checks") + } + + /// Returns `true` when we load mocked POS products for screenshot tests. + static var shouldLoadMockedPOSProducts: Bool { + ProcessInfo.processInfo.arguments.contains("load-mocked-pos-products") + } + + /// Returns `true` when POS order syncing should be bypassed for screenshot tests. + static var shouldBypassPOSOrderSyncing: Bool { + ProcessInfo.processInfo.arguments.contains("bypass-pos-order-syncing") + } + + /// Returns `true` when card present payment service should be mocked for screenshot tests. + static var shouldUseMockCardPresentPayment: Bool { + ProcessInfo.processInfo.arguments.contains("use-mocked-card-present-payment") + } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 1ddce9341f2..92adfcf2d94 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -58,6 +58,11 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { /// Determines whether the POS entry point can be shown based on the selected store and feature gates. func checkEligibility() async -> POSEligibilityState { + // Bypass eligibility checks for screenshot tests + if ProcessConfiguration.shouldBypassPOSEligibilityChecks { + return .eligible + } + async let siteSettingsEligibility = checkSiteSettingsEligibility() async let pluginEligibility = checkPluginEligibility() diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 63f74931a81..7ae0b8aab5d 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -5788,6 +5788,7 @@ 646A2C682E9FCD7E003A32A1 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; 6489D8522EA667AC00D96802 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MultilineEditableTextRow; sourceTree = ""; }; + 684A7F442ECEFDC8003E2A1C /* Mocks */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Mocks; sourceTree = ""; }; DEDB5D342E7A68950022E5A1 /* Bookings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Bookings; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -6769,6 +6770,7 @@ 029327662BF59D2D00D703E7 /* POS */ = { isa = PBXGroup; children = ( + 684A7F442ECEFDC8003E2A1C /* Mocks */, 683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */, 01654EB02E786223001DBB6F /* Adaptors */, 02ABF9B92DF7F8E200348186 /* TabBar */, @@ -13268,6 +13270,7 @@ 2D33E6B02DD1453E000C7198 /* WooShippingPaymentMethod */, 646A2C682E9FCD7E003A32A1 /* Routing */, 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */, + 684A7F442ECEFDC8003E2A1C /* Mocks */, DEDB5D342E7A68950022E5A1 /* Bookings */, ); name = WooCommerce; diff --git a/WooCommerce/WooCommerceScreenshots/WooCommerceScreenshots.swift b/WooCommerce/WooCommerceScreenshots/WooCommerceScreenshots.swift index 412d45fa493..a10f482f3bf 100644 --- a/WooCommerce/WooCommerceScreenshots/WooCommerceScreenshots.swift +++ b/WooCommerce/WooCommerceScreenshots/WooCommerceScreenshots.swift @@ -27,6 +27,10 @@ class WooCommerceScreenshots: XCTestCase { app.launchArguments.append("-simulate-stripe-card-reader") app.launchArguments.append("disable-animations") app.launchArguments.append("-mocks-push-notification") + app.launchArguments.append("bypass-pos-eligibility-checks") + app.launchArguments.append("load-mocked-pos-products") + app.launchArguments.append("bypass-pos-order-syncing") + app.launchArguments.append("bypass-card-present-payment") app.launchArguments.append(contentsOf: ["-mocks-port", "\(server.listenAddress.port)"]) app.launch() @@ -70,6 +74,26 @@ class WooCommerceScreenshots: XCTestCase { .goBackToOrderScreen() .goBackToOrdersScreen() + // POS + try TabNavComponent() + .goToPOSScreen() + .tapAddProduct(productID: 1) + .tapAddProduct(productID: 2) + .thenTakeScreenshot(named: "pos-dashboard", orientation: .landscapeLeft) + .tapConnectReader() + .waitForReaderConnected() + .tapCheckout() + .waitForTotalsLoaded() + .waitForCardPaymentReady() + .thenTakeScreenshot(named: "pos-payment", orientation: .landscapeLeft) + .tapCashPayment() + .tapMarkPaymentComplete() + .waitForPaymentSuccess() + .thenTakeScreenshot(named: "pos-success", orientation: .landscapeLeft) + .tapMenuButton() + .tapExitMenuItem() + .confirmExitPOS() + // Products try TabNavComponent() .goToProductsScreen() @@ -150,7 +174,8 @@ fileprivate var screenshotCount = 0 extension BaseScreen { @MainActor @discardableResult - func thenTakeScreenshot(named title: String) -> Self { + func thenTakeScreenshot(named title: String, orientation: UIDeviceOrientation = .portrait) -> Self { + XCUIDevice.shared.orientation = orientation screenshotCount += 1 let mode = XCUIDevice.inDarkMode ? "dark" : "light" @@ -185,7 +210,8 @@ extension ScreenObject { } @MainActor @discardableResult - func thenTakeScreenshot(named title: String) -> Self { + func thenTakeScreenshot(named title: String, orientation: UIDeviceOrientation = .portrait) -> Self { + XCUIDevice.shared.orientation = orientation screenshotCount += 1 let mode = XCUIDevice.inDarkMode ? "dark" : "light"