Skip to content
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -89,6 +90,15 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController

@MainActor
private func loadRootItems() async {
// Temporay: Bypass product loading for screenshot tests
if ProcessInfo.processInfo.arguments.contains("bypass-pos-product-loading") {
let mockItems = Self.makeScreenshotMockItems()
itemsViewState.containerState = .content
itemsViewState.itemsStack = ItemsStackState(root: .loaded(mockItems, hasMoreItems: false),
itemStates: [:])
return
}

do {
try await paginationTracker.resync { [weak self] pageNumber in
guard let self else { return true }
Expand Down Expand Up @@ -324,3 +334,68 @@ private extension PointOfSaleItemsController {
itemsViewState.itemsStack.itemStates = itemStates
}
}

// MARK: - Screenshot Mock Data
private extension PointOfSaleItemsController {
/// Creates mock POSItems for screenshot tests
/// TODO: Move to ScreenshotsObjectGraph? Or similar.
static func makeScreenshotMockItems() -> [POSItem] {
let port = UserDefaults.standard.integer(forKey: "mocks-port")
let mockResourceUrlHost = "http://localhost:\(port)/"

let product1 = 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 = 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 = 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 = 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)
]
}
}
1 change: 1 addition & 0 deletions Modules/Sources/PointOfSale/Presentation/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ struct CartView: View {
})
.background(backgroundColor.ignoresSafeArea(.all))
.accessibilityElement(children: .contain)
.accessibilityIdentifier("pos-cart-view")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public final class SingleOrderScreen: ScreenObject {
return try PaymentMethodsScreen()
}

@discardableResult
public func goBackToOrdersScreen() throws -> OrdersScreen {
let orderDetailTableView = app.tables["order-details-table-view"]

Expand Down
16 changes: 16 additions & 0 deletions Modules/Sources/UITestsFoundation/Screens/POS/POSScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ScreenObject
import XCTest

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
)
}
}
10 changes: 10 additions & 0 deletions Modules/Sources/UITestsFoundation/Screens/TabNavComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public final class TabNavComponent: ScreenObject {
private let productsTabButtonGetter: (XCUIApplication) -> XCUIElement = {
$0.tabBars.firstMatch.buttons["tab-bar-products-item"]
}

private let posTabButtonGetter: (XCUIApplication) -> XCUIElement = {
$0.tabBars.firstMatch.buttons["tab-bar-pos-item"]
}

private let menuTabButtonGetter: (XCUIApplication) -> XCUIElement = {
$0.tabBars.firstMatch.buttons["tab-bar-menu-item"]
Expand All @@ -23,6 +27,7 @@ public final class TabNavComponent: ScreenObject {
private var ordersTabButton: XCUIElement { ordersTabButtonGetter(app) }
private var menuTabButton: XCUIElement { menuTabButtonGetter(app) }
private var productsTabButton: XCUIElement { productsTabButtonGetter(app) }
private var posTabButton: XCUIElement { posTabButtonGetter(app) }

public init(app: XCUIApplication = XCUIApplication()) throws {
try super.init(
Expand Down Expand Up @@ -53,6 +58,11 @@ public final class TabNavComponent: ScreenObject {
return try ProductsScreen()
}

public func goToPOSScreen() throws -> POSScreen {
posTabButton.tap()
return try POSScreen()
}

@discardableResult
public func goToMenuScreen() throws -> MenuScreen {
menuTabButton.tap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ struct MockAppSettingsActionHandler: MockActionHandler {
case .upsertProductsSettings(_, _, _, _, _, _, _, let onCompletion):
onCompletion(nil)
case .resetEligibilityErrorInfo,
.setTelemetryAvailability
:
.setTelemetryAvailability,
.getAppPasswordsExperimentSettingState,
.getPOSSurveyCurrentMerchantNotificationScheduled,
.getPOSSurveyPotentialMerchantNotificationScheduled,
.getHasPOSBeenOpenedAtLeastOnce,
.setHasPOSBeenOpenedAtLeastOnce,
.setPOSLastOpenedDate:
break
default: unimplementedAction(action: action)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -51,4 +53,8 @@ struct MockCardPresentPaymentActionHandler: MockActionHandler {
let cardReaders = objectGraph.cardReaders
onCompletion(cardReaders)
}

private func observeCardReaderUpdateState(onCompletion: @escaping (AnyPublisher<Yosemite.CardReaderSoftwareUpdateState, Never>) -> Void) {
onCompletion(Just(.none).eraseToAnyPublisher())
}
}
10 changes: 10 additions & 0 deletions WooCommerce/Classes/System/ProcessConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ 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 POS product loading should be bypassed for screenshot tests.
static var shouldBypassPOSProductLoading: Bool {
ProcessInfo.processInfo.arguments.contains("bypass-pos-product-loading")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
13 changes: 11 additions & 2 deletions WooCommerce/WooCommerceScreenshots/WooCommerceScreenshots.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ 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("bypass-pos-product-loading")
app.launchArguments.append(contentsOf: ["-mocks-port", "\(server.listenAddress.port)"])

app.launch()
Expand All @@ -46,6 +48,11 @@ class WooCommerceScreenshots: XCTestCase {
// The interruption monitor above only activates when the app receives user interaction.
app.tap()

// POS
try TabNavComponent()
.goToPOSScreen()
.thenTakeScreenshot(named: "test-pos-screenshot", orientation: .landscapeLeft)

// My Store
try TabNavComponent()
.goToMyStoreScreen()
Expand Down Expand Up @@ -150,7 +157,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"
Expand Down Expand Up @@ -185,7 +193,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"
Expand Down