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,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)
}
}