Files
MatchLiveTv/native/ios/MatchLiveTvUITests/HubFlowsUITests.swift
T
Emiliano FrascaroandCursor 1185d0ce61 Prepara iOS 2.0.12 build 35 per TestFlight e rafforza i test in produzione.
Forza dirette unlisted sul sito (non YouTube), copertina di test e verifica che in pausa gli spettatori vedano la slate. Alza il numero di build oltre le 33–34 già su App Store Connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 17:54:33 +02:00

728 lines
30 KiB
Swift

import XCTest
/// Pilota il simulatore come un utente (backend da API_BASE_URL).
final class HubFlowsUITests: XCTestCase {
private var app: XCUIApplication!
private let shotDir = "/tmp/mltv-uitest"
private var apiBaseUrl = "http://localhost:3000"
override func setUpWithError() throws {
continueAfterFailure = false
addUIInterruptionMonitor(withDescription: "Dialog di sistema") { alert in
for label in [
"Non ora", "Not Now", "Not now",
"Consenti", "OK", "Allow", "Allow Once", "Consenti una volta",
"Allow While Using App", "Consenti durante l'utilizzo",
"Allow While Using the App"
] {
if alert.buttons[label].exists {
alert.buttons[label].tap()
return true
}
}
return false
}
try FileManager.default.createDirectory(atPath: shotDir, withIntermediateDirectories: true)
app = XCUIApplication()
// L'URL API arriva dall'Info.plist dell'app ($(API_BASE_URL) di xcodebuild).
// Non iniettare un default localhost: maschererebbe il collaudo.
if let fromEnv = ProcessInfo.processInfo.environment["API_BASE_URL"]?.trimmingCharacters(in: .whitespacesAndNewlines),
!fromEnv.isEmpty {
apiBaseUrl = fromEnv
app.launchEnvironment["API_BASE_URL"] = fromEnv
}
app.launchArguments = ["--uitesting-reset", "-AppleLanguages", "(it)", "-AppleLocale", "it_IT"]
app.launch()
}
func testLanguagesOnLogin() throws {
waitLogin()
dismissKeyboard()
capture("lang-00-it-login")
XCTAssertTrue(
app.staticTexts["ACCEDI"].exists || app.buttons["ACCEDI"].exists,
"Login italiano atteso (ACCEDI)"
)
XCTAssertTrue(any("login.forgot").exists || app.buttons["Password dimenticata?"].exists)
assertLanguageSwitch(
code: "en",
loginTitle: "LOG IN",
forgot: "Forgot password?",
shot: "lang-01-en-login"
)
assertLanguageSwitch(
code: "fr",
loginTitle: "CONNEXION",
forgot: "Mot de passe oublié ?",
shot: "lang-02-fr-login"
)
assertLanguageSwitch(
code: "de",
loginTitle: "ANMELDEN",
forgot: "Passwort vergessen?",
shot: "lang-03-de-login"
)
assertLanguageSwitch(
code: "es",
loginTitle: "ACCEDER",
forgot: "¿Olvidaste la contraseña?",
shot: "lang-04-es-login"
)
assertLanguageSwitch(
code: "it",
loginTitle: "ACCEDI",
forgot: "Password dimenticata?",
shot: "lang-05-it-login"
)
}
func testCoachFlows() throws {
waitLogin()
dismissKeyboard()
capture("01-login")
let forgot = firstExisting(["login.forgot"], labels: ["Password dimenticata?", "Forgot password?"])
XCTAssertTrue(forgot.waitForExistence(timeout: 8), "Link password dimenticata assente")
tapHittable(forgot)
XCTAssertTrue(any("forgot.back").waitForExistence(timeout: 8), "Schermata password dimenticata assente")
capture("02-forgot-password")
let mail = app.textFields.firstMatch
XCTAssertTrue(mail.waitForExistence(timeout: 4))
tapHittable(mail)
mail.typeText("coach@matchlivetv.test")
dismissKeyboard()
tapHittable(firstExisting(["forgot.submit"], labels: ["Invia link di reset", "Send reset link"]))
XCTAssertTrue(
app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'link'")).firstMatch.waitForExistence(timeout: 12)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'registrat'")).firstMatch.exists,
"Manca il messaggio di esito del reset password.\n\(app.debugDescription)"
)
capture("02b-forgot-submitted")
tapHittable(app.buttons["forgot.back"])
loginIfNeeded()
waitHub()
dismissSystemDialogs()
capture("03-hub-dopo-login")
openLanguagePicker(from: "matches.language")
tapHittable(any("language.en"))
XCTAssertTrue(
app.buttons["NEW MATCH"].waitForExistence(timeout: 8) || any("matches.new").waitForExistence(timeout: 4),
"Hub non passato in inglese"
)
capture("03b-hub-en")
openLanguagePicker(from: "matches.language")
tapHittable(any("language.it"))
XCTAssertTrue(
app.buttons["NUOVA PARTITA"].waitForExistence(timeout: 8) || any("matches.new").waitForExistence(timeout: 4),
"Hub non tornato in italiano"
)
openAccountAndBack()
capture("04-hub-dopo-account")
scheduleMatchShowsConfigureDialog()
capture("05-dopo-programmazione")
}
func testLogoutReturnsToLogin() throws {
loginIfNeeded()
waitHub()
dismissSystemDialogs()
tapHittable(firstExisting(["matches.account"], labels: ["Account"]))
XCTAssertTrue(any("account.back").waitForExistence(timeout: 8), "Account non aperto")
app.swipeUp()
app.swipeUp()
XCTAssertTrue(any("account.logout").waitForExistence(timeout: 6), "Bottone Esci assente")
tapHittable(any("account.logout"))
XCTAssertTrue(
any("login.submit").waitForExistence(timeout: 12) || app.staticTexts["ACCEDI"].waitForExistence(timeout: 4),
"Logout non ha riportato al login"
)
capture("14-dopo-logout")
}
func testWizardNetworkAndLive() throws {
loginIfNeeded()
waitHub()
dismissSystemDialogs()
tapNewMatch()
let quick = firstExisting(["sheet.quick.option"], labels: ["Avvia subito"])
XCTAssertTrue(quick.waitForExistence(timeout: 8))
tapHittable(quick)
XCTAssertTrue(
app.staticTexts["01 · Partita"].waitForExistence(timeout: 25),
"Wizard step 1 assente"
)
capture("06-wizard-step1")
let opponent = any("wizard.opponent").exists ? any("wizard.opponent") : app.textFields.firstMatch
if opponent.waitForExistence(timeout: 8) {
tapHittable(opponent)
if let current = opponent.value as? String, !current.isEmpty {
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: current.count)
opponent.typeText(deletes)
}
opponent.typeText("UI Wizard")
dismissKeyboard()
}
applyMatchCover()
scrollToId("wizard.next")
tapHittable(any("wizard.next"))
XCTAssertTrue(
app.staticTexts["02 · Trasmissione"].waitForExistence(timeout: 20)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Trasmissione' OR label CONTAINS 'Transmission'")).firstMatch.waitForExistence(timeout: 8),
"Wizard step 2 (trasmissione) non aperto.\n\(app.debugDescription)"
)
capture("07-wizard-step2")
if any("wizard.platform.site").waitForExistence(timeout: 4) {
tapHittable(any("wizard.platform.site"))
}
let unlisted = firstExisting(["wizard.privacy.unlisted"], labels: ["NON IN ELENCO", "UNLISTED"])
XCTAssertTrue(unlisted.waitForExistence(timeout: 6), "Manca l'opzione NON IN ELENCO")
tapHittable(unlisted)
XCTAssertTrue(
app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Solo chi ha il link' OR label CONTAINS 'Only those with the link'")).firstMatch.waitForExistence(timeout: 4)
|| any("wizard.privacy.unlisted").exists,
"Visibilità non impostata su privato/unlisted"
)
capture("07b-privacy-unlisted")
scrollToId("wizard.next")
tapHittable(any("wizard.next"))
XCTAssertTrue(
app.staticTexts["03 · Test rete"].waitForExistence(timeout: 20)
|| any("wizard.network.test").waitForExistence(timeout: 10),
"Wizard step 3 (rete) non aperto.\n\(app.debugDescription)"
)
capture("08-wizard-step3")
scrollToId("wizard.network.test")
tapHittable(any("wizard.network.test"))
XCTAssertTrue(
app.staticTexts["PRONTO PER ANDARE IN DIRETTA"].waitForExistence(timeout: 20)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'PRONTO' OR label CONTAINS 'READY'")).firstMatch.waitForExistence(timeout: 10),
"Test rete non completato.\n\(app.debugDescription)"
)
capture("08b-network-ready")
let tree = app.debugDescription
XCTAssertFalse(tree.contains("youtube.com"), "Il test ha creato un video YouTube: deve restare sul sito, unlisted")
XCTAssertTrue(
tree.contains("matchlivetv.it/live") || tree.contains("/live/"),
"Manca il link diretta sul sito"
)
let sessionId = extractLiveSessionId()
XCTAssertNotNil(sessionId, "Impossibile leggere l'id sessione dal link diretta.\n\(tree)")
if let sessionId {
try? sessionId.write(toFile: "\(shotDir)/live-session-id.txt", atomically: true, encoding: .utf8)
assertLivePageIsUnlistedWithMatchCover(sessionId: sessionId)
}
let start = any("wizard.start")
XCTAssertTrue(start.waitForExistence(timeout: 6))
tapHittable(start)
dismissSystemDialogs()
let liveVisible =
any("broadcast.terminate").waitForExistence(timeout: 25)
|| any("broadcast.pause").waitForExistence(timeout: 4)
|| app.buttons["CONCEDI PERMESSI"].waitForExistence(timeout: 4)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'camera' OR label CONTAINS 'microfono' OR label CONTAINS 'Permessi'")).firstMatch.waitForExistence(timeout: 4)
capture("09-broadcast")
XCTAssertTrue(liveVisible, "Dopo INIZIA non si apre la diretta né la richiesta permessi.\n\(app.debugDescription)")
if any("broadcast.mute").waitForExistence(timeout: 3) {
tapHittable(any("broadcast.mute"))
sleep(1)
capture("09b-mute-confirm")
if app.buttons["Conferma"].waitForExistence(timeout: 2) || app.alerts.buttons.element(boundBy: 1).exists {
let confirm = app.buttons["Conferma"].exists ? app.buttons["Conferma"] : app.alerts.buttons.element(boundBy: 1)
tapHittable(confirm)
}
capture("09c-muted")
}
if any("broadcast.score.home.plus").waitForExistence(timeout: 3) {
tapHittable(any("broadcast.score.home.plus"))
sleep(1)
capture("09c2-score-plus")
}
if any("broadcast.abr").waitForExistence(timeout: 2) {
tapHittable(any("broadcast.abr"))
sleep(1)
capture("09c3-abr")
if app.buttons["Annulla"].waitForExistence(timeout: 2) {
tapHittable(app.buttons["Annulla"])
}
}
if any("broadcast.share").waitForExistence(timeout: 2) {
tapHittable(any("broadcast.share"))
sleep(1)
if app.buttons["Conferma"].waitForExistence(timeout: 3) {
tapHittable(app.buttons["Conferma"])
}
sleep(1)
capture("09c4-share")
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
for label in ["Chiudi", "Close", "Annulla", "Cancel"] {
if spring.buttons[label].exists {
spring.buttons[label].tap()
break
}
if app.buttons[label].exists {
tapHittable(app.buttons[label])
break
}
}
}
if any("broadcast.pause").waitForExistence(timeout: 4) {
tapHittable(any("broadcast.pause"))
sleep(1)
capture("09c5-pause-confirm")
let confirmPause = firstExisting([], labels: ["Conferma", "Confirm"])
if confirmPause.waitForExistence(timeout: 3) {
tapHittable(confirmPause)
}
sleep(3)
capture("09c6-paused")
XCTAssertTrue(
app.staticTexts["PAUSA"].waitForExistence(timeout: 8)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'PAUSA' OR label CONTAINS 'PAUSED'")).firstMatch.exists,
"Dopo la conferma non compare lo stato PAUSA"
)
if let sessionId {
assertViewerShowsCoverWhilePaused(sessionId: sessionId)
}
}
if any("broadcast.terminate").waitForExistence(timeout: 3) {
tapHittable(any("broadcast.terminate"))
sleep(1)
capture("09d-terminate-confirm")
for label in ["TERMINA", "END", "BEENDEN", "TERMINAR", "TERMINER"] {
if app.buttons[label].exists {
tapHittable(app.buttons[label])
break
}
}
} else if any("wizard.close").waitForExistence(timeout: 2) {
tapHittable(any("wizard.close"))
}
_ = any("matches.new").waitForExistence(timeout: 12)
capture("09e-back-hub")
}
private func applyMatchCover() {
scrollToId("wizard.cover.change")
let change = firstExisting(["wizard.cover.change"], labels: ["Cambia copertina per questa partita"])
XCTAssertTrue(change.waitForExistence(timeout: 8), "Manca Cambia copertina per questa partita")
scrollToId("wizard.cover.uitest")
let testCover = any("wizard.cover.uitest")
XCTAssertTrue(testCover.waitForExistence(timeout: 6), "Manca il fallback copertina di test")
tapHittable(testCover)
XCTAssertTrue(
app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
).firstMatch.waitForExistence(timeout: 8),
"La copertina di test non è stata applicata a questa partita.\n\(app.debugDescription)"
)
capture("06c-cover-match")
tapHittable(change)
sleep(2)
capture("06b-cover-picker")
_ = pickFirstPhotoFromSystemPicker()
dismissPhotoPicker()
XCTAssertTrue(
app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
).firstMatch.waitForExistence(timeout: 6),
"Dopo il selettore foto la copertina della partita è sparita"
)
}
@discardableResult
private func pickFirstPhotoFromSystemPicker() -> Bool {
let photos = XCUIApplication(bundleIdentifier: "com.apple.PhotosUIService")
guard photos.wait(for: .runningForeground, timeout: 5) else { return false }
let candidates: [XCUIElement] = [
photos.images.firstMatch,
photos.collectionViews.cells.firstMatch,
photos.cells.firstMatch
]
for el in candidates {
if el.waitForExistence(timeout: 2) {
el.tap()
sleep(1)
for label in ["Aggiungi", "Add", "Scegli", "Choose", "Done", "Fine"] {
if photos.buttons[label].exists {
photos.buttons[label].tap()
break
}
}
let source = app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'Questa partita' OR label CONTAINS 'This match'")
).firstMatch
if source.waitForExistence(timeout: 4) { return true }
}
}
return false
}
private func dismissPhotoPicker() {
let photos = XCUIApplication(bundleIdentifier: "com.apple.PhotosUIService")
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
for label in ["Annulla", "Cancel", "Chiudi", "Close"] {
if photos.buttons[label].exists {
photos.buttons[label].tap()
sleep(1)
return
}
if spring.buttons[label].exists {
spring.buttons[label].tap()
sleep(1)
return
}
if app.buttons[label].exists {
tapHittable(app.buttons[label])
sleep(1)
return
}
}
if photos.state == .runningForeground {
photos.swipeDown()
sleep(1)
}
}
private func extractLiveSessionId() -> String? {
let share = any("wizard.network.shareurl")
let raw = share.exists ? share.label : app.debugDescription
guard let range = raw.range(of: #"https?://[^\s]+/live/[0-9a-fA-F-]{36}"#, options: .regularExpression) else {
return nil
}
let url = String(raw[range])
return url.components(separatedBy: "/live/").last
}
private func assertLivePageIsUnlistedWithMatchCover(sessionId: String) {
let pageURL = "https://www.matchlivetv.it/live/\(sessionId)"
let deadline = Date().addingTimeInterval(40)
var html = ""
var poster = ""
while Date() < deadline {
html = httpGet(pageURL) ?? ""
if html.contains("youtube.com/watch") {
XCTFail("La pagina pubblica punta a YouTube invece che al sito")
return
}
if let range = html.range(of: #"poster="([^"]+)""#, options: .regularExpression) {
poster = String(html[range])
.replacingOccurrences(of: "poster=\"", with: "")
.replacingOccurrences(of: "\"", with: "")
}
let unlisted = html.contains("noindex")
let customPoster = !poster.isEmpty && !poster.contains("copertina-canale")
if unlisted && customPoster { break }
RunLoop.current.run(until: Date().addingTimeInterval(2))
}
XCTAssertTrue(html.contains("noindex") || html.contains("nofollow"), "La diretta deve restare non in elenco (noindex)")
XCTAssertFalse(poster.isEmpty, "Manca l'attributo poster sul player live")
XCTAssertFalse(poster.contains("copertina-canale"), "Il poster è ancora la copertina di default: \(poster)")
try? html.write(toFile: "\(shotDir)/live-page.html", atomically: true, encoding: .utf8)
try? poster.write(toFile: "\(shotDir)/live-poster.txt", atomically: true, encoding: .utf8)
}
private func assertViewerShowsCoverWhilePaused(sessionId: String) {
let statusURL = "https://www.matchlivetv.it/live/\(sessionId)/status.json"
let deadline = Date().addingTimeInterval(25)
var payload: [String: Any] = [:]
while Date() < deadline {
payload = httpGetJSON(statusURL) ?? [:]
let paused = payload["paused"] as? Bool ?? false
let status = payload["status"] as? String ?? ""
let showingCover = payload["showing_cover"] as? Bool ?? false
if (paused || status == "paused") && showingCover {
break
}
RunLoop.current.run(until: Date().addingTimeInterval(1.5))
}
if let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted]),
let text = String(data: data, encoding: .utf8) {
try? text.write(toFile: "\(shotDir)/live-status-paused.json", atomically: true, encoding: .utf8)
}
let paused = payload["paused"] as? Bool ?? false
let status = payload["status"] as? String ?? ""
let showingCover = payload["showing_cover"] as? Bool ?? false
XCTAssertTrue(paused || status == "paused", "status.json non è in pausa: \(payload)")
XCTAssertTrue(showingCover, "Gli spettatori non stanno vedendo la copertina (showing_cover=false): \(payload)")
}
private func httpGet(_ urlString: String) -> String? {
guard let url = URL(string: urlString) else { return nil }
let sem = DispatchSemaphore(value: 0)
var result: String?
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data { result = String(data: data, encoding: .utf8) }
sem.signal()
}.resume()
_ = sem.wait(timeout: .now() + 15)
return result
}
private func httpGetJSON(_ urlString: String) -> [String: Any]? {
guard let body = httpGet(urlString),
let data = body.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return json
}
func testResumeLiveSheet() throws {
loginIfNeeded()
waitHub()
dismissSystemDialogs()
capture("10-hub-con-banner")
let banner = app.buttons["matches.resume.banner"]
if !banner.waitForExistence(timeout: 10) {
throw XCTSkip("Nessuna sessione attiva da riprendere su \(apiBaseUrl)")
}
tapHittable(banner)
sleep(1)
capture("11-dopo-tap-banner")
let sheetVisible =
any("resume.sheet").waitForExistence(timeout: 4)
|| any("resume.sheet.panel").waitForExistence(timeout: 2)
|| any("resume.camera").waitForExistence(timeout: 2)
|| app.staticTexts["Diretta in corso"].waitForExistence(timeout: 2)
XCTAssertTrue(sheetVisible, "Lo sheet di ripresa non è comparso.\n\(app.debugDescription)")
capture("12-resume-sheet")
let cancel = firstExisting(["resume.cancel"], labels: ["Annulla", "Cancel"])
if cancel.waitForExistence(timeout: 3) {
tapHittable(cancel)
}
}
private func assertLanguageSwitch(code: String, loginTitle: String, forgot: String, shot: String) {
openLanguagePicker(from: "login.language")
let row = any("language.\(code)")
XCTAssertTrue(row.waitForExistence(timeout: 6), "Voce lingua \(code) assente")
tapHittable(row)
XCTAssertTrue(
app.staticTexts[loginTitle].waitForExistence(timeout: 8) || app.buttons[loginTitle].waitForExistence(timeout: 2),
"Dopo lingua \(code) manca il titolo \(loginTitle).\n\(app.debugDescription)"
)
XCTAssertTrue(
any("login.forgot").waitForExistence(timeout: 4)
|| app.buttons[forgot].waitForExistence(timeout: 2)
|| app.staticTexts[forgot].waitForExistence(timeout: 1),
"Dopo lingua \(code) manca '\(forgot)'"
)
capture(shot)
}
private func openLanguagePicker(from id: String) {
let globe = any(id)
XCTAssertTrue(globe.waitForExistence(timeout: 8), "Bottone lingua \(id) assente")
tapHittable(globe)
XCTAssertTrue(
any("language.it").waitForExistence(timeout: 8) || app.staticTexts["Italiano"].waitForExistence(timeout: 4),
"Picker lingua non aperto"
)
}
private func loginIfNeeded() {
if hubVisible() { return }
waitLogin()
dismissKeyboard()
let email = app.textFields.firstMatch
XCTAssertTrue(email.waitForExistence(timeout: 8))
tapHittable(email)
email.typeText("coach@matchlivetv.test")
let password = app.secureTextFields.firstMatch
XCTAssertTrue(password.waitForExistence(timeout: 4))
tapHittable(password)
password.typeText("Password123")
dismissKeyboard()
let submit = firstExisting(["login.submit"], labels: ["ACCEDI", "LOG IN", "CONNEXION", "ANMELDEN", "ACCEDER"])
XCTAssertTrue(submit.waitForExistence(timeout: 4))
tapHittable(submit)
dismissSystemDialogs()
}
private func waitLogin() {
let deadline = Date().addingTimeInterval(20)
while Date() < deadline {
if any("login.submit").exists || app.textFields.firstMatch.exists {
return
}
if hubVisible() { return }
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
}
capture("fail-login")
XCTFail("Login non comparso. Gerarchia:\n\(app.debugDescription)")
}
private func waitHub() {
XCTAssertTrue(
any("matches.new").waitForExistence(timeout: 25)
|| app.buttons["NUOVA PARTITA"].waitForExistence(timeout: 5)
|| app.buttons["NEW MATCH"].waitForExistence(timeout: 2),
"Hub partite non raggiunto dopo login su \(apiBaseUrl).\n\(app.debugDescription)"
)
dismissKeyboard()
}
private func hubVisible() -> Bool {
app.buttons["matches.new"].exists || app.buttons["NUOVA PARTITA"].exists || app.buttons["NEW MATCH"].exists
}
private func openAccountAndBack() {
let account = firstExisting(["matches.account"], labels: ["Account"])
XCTAssertTrue(account.waitForExistence(timeout: 6), "Bottone Account assente")
tapHittable(account)
XCTAssertTrue(
any("account.title").waitForExistence(timeout: 8) || any("account.back").waitForExistence(timeout: 2),
"Schermata Account non aperta.\n\(app.debugDescription)"
)
capture("04a-account")
XCTAssertTrue(any("account.save.profile").waitForExistence(timeout: 4), "Manca Salva profilo")
tapHittable(any("account.save.profile"))
sleep(2)
capture("04b-account-saved")
tapHittable(app.buttons["account.back"])
XCTAssertTrue(any("matches.new").waitForExistence(timeout: 8))
}
private func scheduleMatchShowsConfigureDialog() {
tapNewMatch()
capture("05a-nuova-partita")
let schedule = firstExisting(["sheet.schedule.option"], labels: ["Programma partita"])
XCTAssertTrue(schedule.waitForExistence(timeout: 8), "Opzione Programma partita assente")
tapHittable(schedule)
let opponent = app.textFields["Avversario"].exists ? app.textFields["Avversario"] : app.textFields.firstMatch
XCTAssertTrue(opponent.waitForExistence(timeout: 8), "Campo avversario assente")
tapHittable(opponent)
opponent.typeText("UI Test Programmata")
dismissKeyboard()
let save = app.buttons["SALVA IN PROGRAMMA"]
XCTAssertTrue(save.waitForExistence(timeout: 4))
tapHittable(save)
XCTAssertTrue(
app.staticTexts["Configurare ora?"].waitForExistence(timeout: 12) || app.alerts.firstMatch.waitForExistence(timeout: 4),
"Manca il dialog Configura/Più tardi"
)
capture("05b-dialog-configura")
let later = app.buttons["Più tardi"].exists ? app.buttons["Più tardi"] : app.alerts.buttons.element(boundBy: 0)
tapHittable(later)
XCTAssertTrue(any("matches.new").waitForExistence(timeout: 8))
XCTAssertTrue(
app.staticTexts["UI Test Programmata"].waitForExistence(timeout: 8)
|| app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Programmata'")).firstMatch.exists,
"La partita programmata non compare in hub"
)
}
private func tapNewMatch() {
let neu = firstExisting(["matches.new"], labels: ["NUOVA PARTITA", "NEW MATCH"])
XCTAssertTrue(neu.waitForExistence(timeout: 8))
tapHittable(neu)
}
private func scrollToId(_ id: String) {
let el = any(id)
for _ in 0..<8 {
if el.exists && el.isHittable { return }
app.swipeUp()
}
}
private func any(_ id: String) -> XCUIElement {
app.descendants(matching: .any)[id]
}
private func firstExisting(_ ids: [String], labels: [String] = []) -> XCUIElement {
for id in ids {
let el = any(id)
if el.exists { return el }
}
for label in labels {
let button = app.buttons[label]
if button.exists { return button }
let text = app.staticTexts[label]
if text.exists { return text }
}
if let firstId = ids.first {
return any(firstId)
}
return app.buttons[labels.first ?? ""]
}
private func tapHittable(_ element: XCUIElement) {
if element.isHittable {
element.tap()
return
}
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
private func dismissSystemDialogs() {
let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard")
_ = spring.buttons["Non ora"].waitForExistence(timeout: 3)
|| spring.buttons["Not Now"].waitForExistence(timeout: 1)
|| app.alerts.buttons["Non ora"].waitForExistence(timeout: 1)
let labels = ["Non ora", "Not Now", "Not now", "OK", "Consenti", "Allow"]
for _ in 0..<8 {
var tapped = false
for label in labels {
if spring.buttons[label].exists {
spring.buttons[label].tap()
tapped = true
break
}
if app.alerts.buttons[label].exists {
app.alerts.buttons[label].tap()
tapped = true
break
}
}
if !tapped { break }
RunLoop.current.run(until: Date().addingTimeInterval(0.4))
}
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.08)).tap()
}
private func dismissKeyboard() {
if !app.keyboards.firstMatch.exists { return }
for name in ["Fine", "Done", "Return", "Invio"] {
let key = app.keyboards.buttons[name]
if key.exists {
key.tap()
return
}
}
app.swipeDown()
sleep(1)
if app.keyboards.firstMatch.exists {
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.12)).tap()
}
}
private func capture(_ name: String) {
let shot = app.screenshot()
try? shot.pngRepresentation.write(to: URL(fileURLWithPath: "\(shotDir)/\(name).png"))
let attachment = XCTAttachment(screenshot: shot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
try? app.debugDescription.write(toFile: "\(shotDir)/\(name).txt", atomically: true, encoding: .utf8)
}
}