import XCTest /// Covers Store Detail -> add to cart -> Cart -> Checkout, the biggest /// remaining zero-coverage surface (CheckoutView.swift alone is ~5000 /// lines). Requires the standing QA account to have a real saved address /// so Home shows a real store list - see /// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md. final class CartCheckoutFlowTests: XCTestCase { /// The cart is on-device state (`CartState`/`SessionStateStore`, see /// `AppState.swift`), not server-side - it persists across app /// relaunches on this simulator just like the login session (see /// decisions/2026-09-11-ui-test-shared-login-session.md), and keeps /// accumulating real quantities across every manual and automated run /// against the "MARIBA" test store. There's no direct "empty cart" UI /// action - the "Trocar de loja?" alert's "Limpar carrinho e /// adicionar" (`StoreDetailView.swift`) only fires for a genuinely /// different store (`shouldAskForStoreSwitch`, /// `StoreDetailView+Logic.swift`), which this suite never triggers /// since every test targets the same store. So: drain the cart via /// its own per-item "minus" control, once per class (not once per /// test - each drain can take several taps), so every run starts from /// a known-empty cart instead of an ever-growing one. override class func setUp() { super.setUp() let app = XCUIApplication() app.launch() app.ensureLoggedIn() let cartTab = app.images.matching(identifier: "cart.fill").firstMatch guard cartTab.waitForExistence(timeout: 10) else { return } cartTab.tap() guard app.staticTexts["Meu Carrinho"].waitForExistence(timeout: 10) else { return } // No "set quantity" input exists in CartView - only per-tap // increment/decrement - so draining is inherently one tap per // unit. Capped rather than unbounded so a genuinely stuck cart // fails fast instead of hanging the whole class. let minusButton = app.images.matching(identifier: "minus").firstMatch var remainingTaps = 500 while app.staticTexts["Seu carrinho está vazio"].exists == false, remainingTaps > 0 { guard minusButton.waitForExistence(timeout: 3) else { break } minusButton.tap() usleep(150_000) remainingTaps -= 1 } } override func setUpWithError() throws { continueAfterFailure = false } // No per-test logout - see decisions/2026-09-11-ui-test-shared-login-session.md. func testAddProductToCartAndReachCheckout() throws { let app = XCUIApplication() app.launch() try reachCheckoutWithOneItem(app) } /// Exercises CheckoutView's payment-method selection and the address /// picker's "Alterar" entry point without ever tapping "Confirmar e /// Pagar" - deliberately not submitting a real order (per explicit /// user direction: cover the screen, don't create real order data). func testCheckoutPaymentMethodSelectionAndAddressAlterar() throws { let app = XCUIApplication() app.launch() try reachCheckoutWithOneItem(app) XCTAssertTrue(app.staticTexts["MÉTODO DE PAGAMENTO"].waitForExistence(timeout: 5)) // PIX is the default selection; only tap an alternative if this // store's real catalog actually offers one - don't assume Cartão // de Crédito exists (in-app payment methods depend on real store // config, already found to vary - see the standing-account state // notes in decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md). let creditCardRow = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "Cartão de Crédito")).firstMatch if creditCardRow.exists { creditCardRow.tap() } XCTAssertTrue(app.staticTexts["TIPO DE ENTREGA"].waitForExistence(timeout: 5)) app.buttons["Alterar"].tap() XCTAssertTrue( app.staticTexts["Meus Endereços"].waitForExistence(timeout: 10), "Alterar never opened the address picker" ) // Dismiss without changing anything, back to Checkout. .isHittable, // not just .exists: a Back button that exists but is mid-transition // throws a fatal, uncatchable "not hittable" failure on tap - same // issue already fixed in UITestSupport.reachProfileTabRegardlessOfAuthState. // Poll briefly since the push-in animation can still be settling // right after "Meus Endereços" first becomes visible. let backButton = app.buttons.matching(NSPredicate(format: "label == %@", "Back")).firstMatch var dismissed = false for _ in 0..<10 { if backButton.exists, backButton.isHittable { backButton.tap() dismissed = true break } usleep(200_000) } if dismissed == false { app.swipeDown() } if app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10) == false { let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/after_alterar_dismiss.png")) } XCTAssertTrue(app.staticTexts["Finalizar Pedido"].exists, "Never returned to Checkout") // Deliberately not tapping "Confirmar e Pagar" - see doc comment. } /// With "Cartão de Crédito" selected, "Confirmar e Pagar" does NOT /// submit an order immediately - `CheckoutView+Logic.handleConfirmPaymentTap()` /// returns early and opens `CardSelectionSheet` instead (real order /// creation only happens if a saved card is then picked and confirmed /// inside that sheet, which this test does not do). Confirmed by /// reading the source before relying on it - this is a real, /// deliberate app behavior, not an assumption. func testConfirmarEPagarWithCreditCardOpensCardSelectionWithoutSubmitting() throws { let app = XCUIApplication() app.launch() try reachCheckoutWithOneItem(app) let creditCardRow = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "Cartão de Crédito")).firstMatch XCTAssertTrue(creditCardRow.waitForExistence(timeout: 5), "This store is expected to offer Cartão de Crédito") creditCardRow.tap() app.buttons["Confirmar e Pagar"].tap() XCTAssertTrue(app.staticTexts["Selecionar Cartão"].waitForExistence(timeout: 10), "Never reached the card selection sheet") // "Adicionar novo cartão" opens PaymentCardView (CheckoutView.swift's // own card-entry form - a different struct from AddCardFormView.swift, // reached from Profile -> Meus Cartões, needs its own coverage) as a // sheet on top. Only fills nothing and dismisses - no card saved, no // order created. app.buttons["Adicionar novo cartão"].tap() XCTAssertTrue( app.staticTexts["Novo Cartão"].waitForExistence(timeout: 10), "Adicionar novo cartão never opened PaymentCardView" ) dismissViaCloseButton(app) XCTAssertTrue(app.staticTexts["Selecionar Cartão"].waitForExistence(timeout: 10), "Never returned to card selection") dismissViaCloseButton(app) XCTAssertTrue(app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), "Never returned to Checkout") } /// Dismisses via the topmost sheet's "x" close button /// (`identifier: "xmark"`) - shared by `CardSelectionSheet` and /// `PaymentCardView`. `.firstMatch` tolerates a lower sheet's own /// close button still existing underneath in the accessibility tree. private func dismissViaCloseButton(_ app: XCUIApplication) { let closeButton = app.buttons.matching(identifier: "xmark").firstMatch for _ in 0..<10 { if closeButton.exists, closeButton.isHittable { closeButton.tap() return } usleep(200_000) } app.swipeDown() } private func reachCheckoutWithOneItem(_ app: XCUIApplication) throws { XCTAssertTrue(app.ensureLoggedIn(), "Login never completed") // ensureLoggedIn can land on whichever tab it detected the // authenticated session from (e.g. Profile, if already logged in // from a previous test) rather than Home - switch explicitly. XCTAssertTrue(app.buttons["Home"].waitForExistence(timeout: 10)) app.buttons["Home"].tap() // The store card is one merged tappable element (name + category + // distance + status all combine into its accessibility label), not // a plain static text. let storeCard = app.buttons.matching(NSPredicate(format: "label CONTAINS[c] %@", "MARIBA")).firstMatch if storeCard.waitForExistence(timeout: 15) == false { let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/no_store_card.png")) } XCTAssertTrue(storeCard.exists, "Expected store card never appeared on Home") storeCard.tap() // A plain (non-addon, non-pizza) product row's "+" control adds // directly with no sheet. Once a product's quantity is > 0, its // outer Button's identifier moves off itself - the same badge- // merging bug already fixed for the tab bar's cart icon - because // the row's own quantity Text becomes the Button's accessible // label/identity instead. The QA account's cart has genuinely // accumulated real quantities across many runs today, so // app.buttons.matching(identifier: "plus") stopped matching once // the first several products all had quantity > 0 (confirmed via // screenshot: the "+" controls were clearly visible on screen // while the buttons-only query found nothing). The nested Image // keeps identifier "plus" regardless of quantity, so target that // directly instead - same fix pattern as the cart-tab icon. let addButton = app.images.matching(identifier: "plus").firstMatch if addButton.waitForExistence(timeout: 25) == false { let dir = "/private/tmp/claude-501/-Users-loverde-co-Documents-Loverde-JOBs-Producao-Loverde-Co-LC-RAG-Struct-projects-PediFoods-ios/4a9ec4f7-c3ad-4cf5-8dad-073446f6fd81/scratchpad" try? app.screenshot().pngRepresentation.write(to: URL(fileURLWithPath: "\(dir)/store_detail_timeout40.png")) } XCTAssertTrue(addButton.exists, "Store Detail's product list never loaded") addButton.tap() // Once the cart has items, the tab bar button's accessible label // becomes just the badge count ("4") instead of "Shopping Cart" - // confirmed via hierarchy dump (SwiftUI's accessibility-children // combining dropped the nested Image's default "Shopping Cart" // label entirely once a sibling Text badge was added). The nested // "cart.fill" Image keeps its identifier regardless, so target // that directly rather than the outer button. let cartTab = app.images.matching(identifier: "cart.fill").firstMatch XCTAssertTrue(cartTab.waitForExistence(timeout: 5)) cartTab.tap() XCTAssertTrue(app.staticTexts["Meu Carrinho"].waitForExistence(timeout: 10)) XCTAssertFalse(app.staticTexts["Seu carrinho está vazio"].exists, "Cart still shows empty after adding a product") app.buttons["Ir para o Pagamento"].tap() XCTAssertTrue(app.staticTexts["Finalizar Pedido"].waitForExistence(timeout: 10), "Never reached Checkout") } }