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