migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,51 @@
import XCTest
/// Covers screens reachable directly from the authenticated Profile tab
/// with no cart/store-browse setup needed - OrdersView and MyReviewsView
/// both load their own data. The standing QA account has real order/review
/// history from prior manual and automated testing, so these don't assert
/// on empty vs. non-empty - just that the real network load completes
/// without getting stuck. See
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md for the
/// account (`ensureLoggedIn`).
final class AuthenticatedProfileNavigationTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testOrdersScreenReachableAndLoadsRealData() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Never reached authenticated Profile")
app.staticTexts["Meus Pedidos"].tap()
XCTAssertTrue(app.staticTexts["Meus Pedidos"].waitForExistence(timeout: 10))
for _ in 0..<30 {
if app.activityIndicators.firstMatch.exists == false { break }
usleep(500_000)
}
XCTAssertFalse(app.activityIndicators.firstMatch.exists, "Orders list never finished loading")
}
func testReviewsScreenReachableAndLoadsRealData() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Never reached authenticated Profile")
app.staticTexts["Minhas Avaliações"].tap()
XCTAssertTrue(app.staticTexts["Minhas Avaliações"].waitForExistence(timeout: 10))
for _ in 0..<30 {
if app.activityIndicators.firstMatch.exists == false { break }
usleep(500_000)
}
XCTAssertFalse(app.activityIndicators.firstMatch.exists, "Reviews list never finished loading")
}
}

View File

@@ -0,0 +1,23 @@
import XCTest
/// Covers a real, authenticated session with the standing QA account (see
/// TestFixtures.swift / decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md).
/// Logs out in tearDown so guest-only tests elsewhere in the same run don't
/// inherit an authenticated Keychain session.
final class AuthenticatedSessionFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testLoginReachesAuthenticatedProfile() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Profile tab never showed the authenticated (logged-in) state")
}
}

View File

@@ -0,0 +1,62 @@
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 {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testAddProductToCartAndReachCheckout() throws {
let app = XCUIApplication()
app.launch()
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 "+" button adds
// directly with no sheet - identified by its SF Symbol name like
// the rest of this app's icon-only controls (person.fill, etc).
let addButton = app.buttons.matching(identifier: "plus").firstMatch
XCTAssertTrue(addButton.waitForExistence(timeout: 15), "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")
}
}

View File

@@ -0,0 +1,73 @@
import XCTest
/// Covers the guest (unauthenticated) Home screen - confirmed reachable
/// and stable, see decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md.
/// Authenticated-only flows (real store data, cart checkout, orders) are
/// blocked by a separate login-navigation issue documented there and are
/// not covered here.
final class HomeGuestFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testHomeRendersSearchAndGreeting() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachGuestHome(), "Never reached Home's search field")
XCTAssertTrue(app.staticTexts["ENTREGAR EM:"].exists)
XCTAssertTrue(app.textFields["Search menu, restaurant or craving"].exists)
XCTAssertTrue(app.images["bell"].exists, "Notifications bell missing")
}
func testHomeShowsDefaultCategoryChip() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachGuestHome())
XCTAssertTrue(app.staticTexts["Categories"].exists)
XCTAssertTrue(app.staticTexts["Todas"].exists, "Default 'Todas' category chip missing")
}
func testHomeShowsGuestLocationPromptWhenNoLocationChosen() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachGuestHome())
// No guest state/city selected -> Home's own inline message, not
// the blocking sheet (which we just dismissed to get here).
XCTAssertTrue(app.staticTexts["Escolha um estado e cidade para visualizar os estabelecimentos."].waitForExistence(timeout: 3))
}
func testTappingAddressChipReopensLocationPicker() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachGuestHome())
// The "ENTREGAR EM:" address button re-opens the same address
// picker sheet we dismissed to reach Home - confirms the chip is
// wired to the real activeModal state, not just decorative.
let addressChip = app.buttons.matching(NSPredicate(format: "identifier == 'chevron.down'")).firstMatch
if addressChip.exists {
addressChip.tap()
} else {
// Fall back to tapping near the "ENTREGAR EM:" label's row.
app.staticTexts["ENTREGAR EM:"].tap()
}
XCTAssertTrue(
app.staticTexts["Escolha seu estado"].waitForExistence(timeout: 3)
|| app.staticTexts["Escolha sua cidade"].waitForExistence(timeout: 3),
"Tapping the address chip did not reopen the location picker"
)
}
// No testSearchFieldAcceptsTextInput here: the guest address-picker
// sheet re-presents on an independent timer (see
// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md) and
// can land back on top between a tap and the following `typeText`,
// which XCTest treats as a fatal, non-catchable event-synthesis
// failure - unlike `.exists` checks, there is no reliable way to guard
// against it. Confirmed via accessibility-hierarchy dump that the sheet
// was back on top at the moment of failure. Dropped rather than
// papered over with more retries.
}

View File

@@ -0,0 +1,35 @@
import XCTest
/// Covers Orders -> Order Details, another big zero-coverage screen
/// (OrderDetailsView.swift, ~1100 lines). Relies on the standing QA
/// account's real order history - see
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md.
final class OrderDetailsFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testOrderDetailsReachableFromOrdersList() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Never reached authenticated Profile")
app.staticTexts["Meus Pedidos"].tap()
XCTAssertTrue(app.staticTexts["Meus Pedidos"].waitForExistence(timeout: 10))
// "Ver Detalhes" appears once per order card; this account has real
// order history (confirmed in AuthenticatedProfileNavigationTests).
let detailsButton = app.buttons["Ver Detalhes"]
XCTAssertTrue(detailsButton.waitForExistence(timeout: 10), "No orders with a 'Ver Detalhes' action found")
detailsButton.tap()
XCTAssertTrue(app.staticTexts["Detalhes do Pedido"].waitForExistence(timeout: 10), "Never reached Order Details")
XCTAssertTrue(app.staticTexts["Itens do Pedido"].waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["Resumo de Valores"].waitForExistence(timeout: 5))
}
}

View File

@@ -0,0 +1,19 @@
import XCTest
final class PediFoodsUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool { true }
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@@ -0,0 +1,37 @@
import XCTest
/// Covers the Profile tab's logged-out state (`ProfileLoggedOutView`) -
/// confirmed reachable. Actually logging in is blocked by a separate
/// navigation issue in LoginView's entry-reveal animation, documented in
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md - not
/// covered here.
final class ProfileLoggedOutFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLoggedOutProfileShowsLoginPrompt() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachLoggedOutProfile(), "Never reached the logged-out profile screen")
XCTAssertTrue(app.staticTexts["Entre na sua conta"].exists)
XCTAssertTrue(app.staticTexts["Faça login ou cadastre-se para ver seu perfil, pedidos e endereços."].exists)
XCTAssertTrue(app.buttons["Entrar ou Cadastrar"].isHittable)
}
func testTappingLoginButtonDoesNotCrash() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.reachLoggedOutProfile())
app.buttons["Entrar ou Cadastrar"].tap()
sleep(2)
// Known limitation (see decisions doc): the login screen's entry
// animation doesn't reliably reveal when navigating here from an
// already-running guest session, so this only asserts the app is
// still alive and responsive, not that login itself completes.
XCTAssertEqual(app.state, .runningForeground)
}
}

View File

@@ -0,0 +1,29 @@
import XCTest
/// Covers Profile -> Meus Cartões -> Novo Cartão
/// (SavedCardsView.swift + AddCardFormView.swift, ~1170 lines on the form
/// alone, was 0% covered). Only reaches the form, doesn't submit a real
/// card.
final class SavedCardsFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testAddCardFormReachableFromSavedCards() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Never reached authenticated Profile")
app.staticTexts["Meus Cartões"].tap()
XCTAssertTrue(app.staticTexts["Meus Cartões"].waitForExistence(timeout: 10), "Never reached Meus Cartões")
app.staticTexts["Adicionar novo cartão"].tap()
XCTAssertTrue(app.staticTexts["Novo Cartão"].waitForExistence(timeout: 10), "Never reached Novo Cartão form")
XCTAssertTrue(app.staticTexts["Número do cartão"].waitForExistence(timeout: 5))
}
}

View File

@@ -0,0 +1,12 @@
import Foundation
/// Standing QA account for UI tests that need an authenticated session.
/// Fixed OTP, no SMS round trip needed. See
/// decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md for
/// context (this is separate from guest-session App Attest, which has no
/// known bypass yet).
enum UITestAccount {
static let email = "customer003@loverde.com.br"
static let phone = "19991673003"
static let otp = "66667777"
}

View File

@@ -0,0 +1,190 @@
import XCTest
extension XCUIApplication {
/// The guest address-picker sheet (`AddressPickerModalView` ->
/// `PublicLocationPickerView`) is a plain `.sheet`, so it's
/// swipe-dismissible - but `HomeView+Data.loadGuestStores()`
/// re-presents it every time Home reappears with no guest location
/// set (see decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md),
/// so dismissing it is a race against that re-presentation. Retries a
/// swipe-down + Home-tab tap until either Home's search field is
/// reachable or the attempt budget runs out.
@discardableResult
func reachGuestHome(attempts: Int = 8) -> Bool {
let searchField = textFields["Search menu, restaurant or craving"]
// The sheet can reappear a few hundred ms after Home first shows up,
// so a single `.exists` check can pass right before it gets covered
// again - re-check after a short settle delay to confirm it stuck.
func settled() -> Bool {
guard searchField.exists else { return false }
usleep(300_000)
return searchField.exists
}
if settled() { return true }
for _ in 1...attempts {
swipeDown()
usleep(300_000)
if searchField.waitForExistence(timeout: 1), settled() {
return true
}
}
return settled()
}
/// Same race as `reachGuestHome`, but continuing on to tap the
/// Profile tab each attempt until the logged-out profile screen's
/// "Entrar ou Cadastrar" button appears. Only meaningful for a genuine
/// guest session - if a real login persisted (`TokenStore` is
/// Keychain-backed, survives app relaunches), this correctly never
/// succeeds, since it only recognizes the logged-out signal. Use
/// `reachProfileTabRegardlessOfAuthState` for authenticated-flow tests.
@discardableResult
func reachLoggedOutProfile(attempts: Int = 8) -> Bool {
let loginButton = buttons["Entrar ou Cadastrar"]
if loginButton.exists { return true }
for _ in 1...attempts {
swipeDown()
usleep(300_000)
let profileTab = buttons.matching(identifier: "person.fill").element(boundBy: 1)
if profileTab.exists {
profileTab.tap()
}
usleep(500_000)
if loginButton.exists {
return true
}
}
return loginButton.exists
}
/// Same swipe-dismiss-then-tap race as `reachLoggedOutProfile`, but
/// recognizes *either* the logged-out ("Entrar ou Cadastrar") or
/// authenticated ("Sair da Conta") signal as success. Needed because
/// an authenticated account with no saved address hits the exact same
/// re-presenting-sheet race as guests (`HomeView`'s "no address" branch
/// re-shows the address picker on every Home reappearance), just
/// rendering `AddressesView` (a real, scrollable address list) instead
/// of `PublicLocationPickerView` (short, static content). `swipeDown()`
/// alone isn't reliable there - a real `List`/`ScrollView` can consume
/// the drag gesture as a scroll instead of a dismiss, unlike the
/// guest picker's static content - so this taps the modal's own
/// "Back" button (`.appInlineNavigationTitle()`'s `dismiss()`-backed
/// button, present on both variants) first, falling back to
/// `swipeDown()` only if that button isn't there.
@discardableResult
func reachProfileTabRegardlessOfAuthState(attempts: Int = 20) -> Bool {
let loginButton = buttons["Entrar ou Cadastrar"]
let logoutButton = buttons["Sair da Conta"]
if loginButton.exists || logoutButton.exists { return true }
for _ in 1...attempts {
// Exact-match subscript crashes ("multiple matching elements")
// if more than one "Back" button is on screen at once (e.g. a
// pushed detail screen's own back button plus the address
// picker's) - firstMatch tolerates any count.
// .isHittable, not just .exists: tapping a Back button that
// exists but is mid-transition (e.g. multiple pushed screens
// deep, like Profile -> Meus Cartões -> Novo Cartão) throws a
// fatal, uncatchable "not hittable" test failure rather than
// just failing gracefully - confirmed via a real teardown run.
let backButton = buttons.matching(NSPredicate(format: "label == %@", "Back")).firstMatch
if backButton.exists, backButton.isHittable {
backButton.tap()
} else {
swipeDown()
}
usleep(300_000)
// Unlike reachLoggedOutProfile, this must also work once
// authenticated - Home's header avatar shows the account's
// real profile picture instead of a generic "person.fill" icon
// once one is set, dropping the match count from 2 to 1 and
// making a fixed index resolve to nothing. `.last` tolerates
// either count (confirmed via screenshot: a real authenticated
// account with a profile picture only ever produces the tab
// bar's match).
if let profileTab = buttons.matching(identifier: "person.fill").allElementsBoundByIndex.last,
profileTab.exists {
profileTab.tap()
}
usleep(500_000)
if loginButton.exists || logoutButton.exists {
return true
}
}
return loginButton.exists || logoutButton.exists
}
// MARK: - Authenticated-flow helpers
/// Real end-to-end login with the standing QA account (see
/// TestFixtures.swift / decisions/2026-08-06-ui-test-account-and-app-attest-bypass.md).
/// The OTP field sets `.textContentType(.oneTimeCode)`
/// (`appOTPKeyboard()`, `TextFieldCompat.swift`) - the iOS system code
/// content type - which two different input methods failed against in
/// this environment: `.typeText()` delivered zero characters (keyboard
/// showed, field stayed empty), and `UIPasteboard` + the app's own
/// "Colar código" paste button delivered one stray, unrelated digit
/// (likely Simulator's host-Mac pasteboard sync racing the write).
/// Fix: tap the real on-screen numeric keypad's digit keys
/// (`app.keys[...]`) directly instead of any text-injection path -
/// confirmed reliable end to end (real OTP validation, real routing).
@discardableResult
func ensureLoggedIn(
email: String = UITestAccount.email,
phone: String = UITestAccount.phone,
otp: String = UITestAccount.otp,
timeout: TimeInterval = 30
) -> Bool {
guard reachProfileTabRegardlessOfAuthState() else { return false }
if buttons["Sair da Conta"].exists {
return true
}
buttons["Entrar ou Cadastrar"].tap()
let entrarButton = buttons["ENTRAR"]
guard entrarButton.waitForExistence(timeout: 5) else { return false }
entrarButton.tap()
let emailField = textFields["seu@email.com"]
guard emailField.waitForExistence(timeout: 5) else { return false }
emailField.tap()
emailField.typeText(email)
let phoneField = textFields["(00) 00000-0000"]
phoneField.tap()
phoneField.typeText(phone)
buttons["Receber Código"].tap()
guard staticTexts["Verificação"].waitForExistence(timeout: 15) else { return false }
textFields.firstMatch.tap()
guard keys["6"].waitForExistence(timeout: 5) else { return false }
for digit in otp {
keys[String(digit)].tap()
}
return buttons["Home"].waitForExistence(timeout: timeout)
}
/// Counterpart to `ensureLoggedIn` - call from `tearDown()` in any test
/// that logged in, so guest-only tests elsewhere in the same run don't
/// inherit an authenticated Keychain session (`TokenStore` persists
/// across app relaunches). No-op if already logged out.
@discardableResult
func logoutIfAuthenticated() -> Bool {
guard reachProfileTabRegardlessOfAuthState() else { return false }
guard buttons["Sair da Conta"].exists else { return true }
buttons["Sair da Conta"].tap()
let confirm = buttons["Sair"]
guard confirm.waitForExistence(timeout: 3) else { return false }
confirm.tap()
return buttons["Entrar ou Cadastrar"].waitForExistence(timeout: 5)
}
}

View File

@@ -0,0 +1,24 @@
import XCTest
/// Covers Profile -> Meu Perfil (UserProfileView.swift, ~750 lines, was
/// 0.8% covered), reachable via the header's "Ver Perfil" link.
final class UserProfileFlowTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
override func tearDownWithError() throws {
XCUIApplication().logoutIfAuthenticated()
}
func testUserProfileReachableFromProfileHeader() throws {
let app = XCUIApplication()
app.launch()
XCTAssertTrue(app.ensureLoggedIn(), "Login never completed")
XCTAssertTrue(app.buttons["Sair da Conta"].waitForExistence(timeout: 10), "Never reached authenticated Profile")
app.staticTexts["Ver Perfil"].tap()
XCTAssertTrue(app.staticTexts["Meu Perfil"].waitForExistence(timeout: 10), "Never reached Meu Perfil")
}
}