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 testPasswordRejectAndLogout() throws { loginIfNeeded() waitHub() dismissSystemDialogs() tapHittable(firstExisting(["matches.account"], labels: ["Account"])) XCTAssertTrue(any("account.back").waitForExistence(timeout: 8), "Account non aperto") app.swipeUp() app.swipeUp() let secures = app.secureTextFields XCTAssertTrue(secures.element(boundBy: 0).waitForExistence(timeout: 6), "Campi password assenti") tapHittable(secures.element(boundBy: 0)) secures.element(boundBy: 0).typeText("sbagliata") tapHittable(secures.element(boundBy: 1)) secures.element(boundBy: 1).typeText("NuovaPass123!") tapHittable(secures.element(boundBy: 2)) secures.element(boundBy: 2).typeText("NuovaPass123!") dismissKeyboard() tapHittable(firstExisting(["account.save.password"], labels: ["Aggiorna password"])) sleep(2) capture("13-password-reject") XCTAssertTrue( app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] 'password' OR label CONTAINS[c] 'non valid' OR label CONTAINS[c] 'errat' OR label CONTAINS[c] 'incorrect'")).firstMatch.waitForExistence(timeout: 8) || any("account.save.password").exists, "Atteso un errore sul cambio password con password attuale sbagliata" ) 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() } if any("wizard.cover.change").waitForExistence(timeout: 3) || app.buttons["Cambia copertina per questa partita"].waitForExistence(timeout: 2) { let cover = firstExisting(["wizard.cover.change"], labels: ["Cambia copertina per questa partita"]) tapHittable(cover) sleep(1) capture("06b-cover-picker") let spring = XCUIApplication(bundleIdentifier: "com.apple.springboard") for label in ["Annulla", "Cancel", "Chiudi", "Close"] { if spring.buttons[label].exists { spring.buttons[label].tap() break } if app.buttons[label].exists { tapHittable(app.buttons[label]) break } } dismissSystemDialogs() } 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 app.buttons["NON IN ELENCO"].waitForExistence(timeout: 3) { tapHittable(app.buttons["NON IN ELENCO"]) } if app.staticTexts["YouTube Live"].waitForExistence(timeout: 3) { tapHittable(app.staticTexts["YouTube Live"]) sleep(1) capture("07b-youtube-tap") if app.alerts.firstMatch.exists { app.alerts.buttons.firstMatch.tap() } } 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 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.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") } 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) } }