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>
This commit is contained in:
Emiliano Frascaro
2026-08-31 17:54:33 +02:00
co-authored by Cursor
parent 90d481a2d1
commit 1185d0ce61
8 changed files with 257 additions and 37 deletions
@@ -169,25 +169,7 @@ final class HubFlowsUITests: XCTestCase {
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()
}
applyMatchCover()
scrollToId("wizard.next")
tapHittable(any("wizard.next"))
XCTAssertTrue(
@@ -196,13 +178,18 @@ final class HubFlowsUITests: XCTestCase {
"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) {
// Non selezionare YouTube in produzione: solo verifica che la card esista.
capture("07b-youtube-visible")
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(
@@ -220,6 +207,18 @@ final class HubFlowsUITests: XCTestCase {
"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))
@@ -278,6 +277,26 @@ final class HubFlowsUITests: XCTestCase {
}
}
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)
@@ -295,6 +314,174 @@ final class HubFlowsUITests: XCTestCase {
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()