migration
This commit is contained in:
51
PediFoodsUITests/AuthenticatedProfileNavigationTests.swift
Normal file
51
PediFoodsUITests/AuthenticatedProfileNavigationTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
23
PediFoodsUITests/AuthenticatedSessionFlowTests.swift
Normal file
23
PediFoodsUITests/AuthenticatedSessionFlowTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
62
PediFoodsUITests/CartCheckoutFlowTests.swift
Normal file
62
PediFoodsUITests/CartCheckoutFlowTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
73
PediFoodsUITests/HomeGuestFlowTests.swift
Normal file
73
PediFoodsUITests/HomeGuestFlowTests.swift
Normal 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.
|
||||
}
|
||||
35
PediFoodsUITests/OrderDetailsFlowTests.swift
Normal file
35
PediFoodsUITests/OrderDetailsFlowTests.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
19
PediFoodsUITests/PediFoodsUITestsLaunchTests.swift
Normal file
19
PediFoodsUITests/PediFoodsUITestsLaunchTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
37
PediFoodsUITests/ProfileLoggedOutFlowTests.swift
Normal file
37
PediFoodsUITests/ProfileLoggedOutFlowTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
29
PediFoodsUITests/SavedCardsFlowTests.swift
Normal file
29
PediFoodsUITests/SavedCardsFlowTests.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
12
PediFoodsUITests/TestFixtures.swift
Normal file
12
PediFoodsUITests/TestFixtures.swift
Normal 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"
|
||||
}
|
||||
190
PediFoodsUITests/UITestSupport.swift
Normal file
190
PediFoodsUITests/UITestSupport.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
24
PediFoodsUITests/UserProfileFlowTests.swift
Normal file
24
PediFoodsUITests/UserProfileFlowTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user