Aggiunge app iOS nativa con parità multi-sport e test unitari.

Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-14 21:37:32 +02:00
parent b798e0ea06
commit 585332e32e
101 changed files with 7910 additions and 5 deletions

View File

@@ -0,0 +1,47 @@
import XCTest
/// Smoke test eseguibile senza @testable import (validazione logica pura).
final class ApiInstantTests: XCTestCase {
func testOverlayKindMapping() {
XCTAssertEqual(OverlayKindMapping.fromApi("basket"), "basket")
XCTAssertEqual(OverlayKindMapping.fromApi("none"), "none")
XCTAssertEqual(OverlayKindMapping.fromApi(nil), "volley")
}
func testRTMPUrlParser() {
let target = RTMPUrlParserLogic.parse("rtmp://stream.example.com/live/match_abc-123")
XCTAssertEqual(target?.connectURL, "rtmp://stream.example.com/live")
XCTAssertEqual(target?.streamName, "match_abc-123")
}
}
enum OverlayKindMapping {
static func fromApi(_ value: String?) -> String {
switch value?.lowercased() {
case "none", "timer": return "none"
case "basket": return "basket"
case "timed": return "timed"
case "racket": return "racket"
default: return "volley"
}
}
}
enum RTMPUrlParserLogic {
struct Target {
let connectURL: String
let streamName: String
}
static func parse(_ urlString: String) -> Target? {
guard let url = URL(string: urlString), url.scheme?.hasPrefix("rtmp") == true else { return nil }
let pathParts = url.path.split(separator: "/").map(String.init)
guard pathParts.count >= 2 else { return nil }
let app = pathParts.dropLast().joined(separator: "/")
let streamName = pathParts.last ?? ""
var connect = "rtmp://\(url.host ?? "")"
if let port = url.port { connect += ":\(port)" }
connect += "/\(app)"
return Target(connectURL: connect, streamName: streamName)
}
}

View File

@@ -0,0 +1,140 @@
import XCTest
@testable import MatchLiveTv
final class MatchScoringRulesTests: XCTestCase {
private func volleyMatch(setsToWin: Int = 3, rules: ScoringRules? = nil) -> Match {
Match(
id: "match-1",
teamId: "team-1",
teamName: "Tigers Volley",
opponentName: "Avversario",
location: nil,
scheduledAt: nil,
sportKey: "pallavolo",
sportLabel: "Pallavolo",
boardType: "volley",
overlayKind: "volley",
effectiveOverlayKind: "volley",
setsToWin: setsToWin,
category: nil,
scoringRules: rules,
activeSessionId: nil,
activeSessionStatus: nil,
streamCompleted: false,
coachHubVisible: nil,
homePrimaryColor: "#FF2D2D",
homeSecondaryColor: nil,
homeLogoUrl: nil,
opponentPrimaryColor: "#1E3A8A",
opponentLogoUrl: nil
)
}
func testSetWinnerAt26_12() {
let rules = MatchScoringContext(match: volleyMatch())
XCTAssertEqual(rules.setWinnerFromPoints(homePoints: 26, awayPoints: 12, currentSet: 1), .home)
}
func testSetWinnerRequiresMinLead() {
let rules = MatchScoringContext(match: volleyMatch())
XCTAssertNil(rules.setWinnerFromPoints(homePoints: 25, awayPoints: 24, currentSet: 1))
XCTAssertEqual(rules.setWinnerFromPoints(homePoints: 25, awayPoints: 23, currentSet: 1), .home)
}
func testDecidingSetUses15Points() {
let rules = MatchScoringContext(match: volleyMatch(setsToWin: 3))
XCTAssertEqual(rules.maxSets, 5)
XCTAssertEqual(rules.pointsTarget(currentSet: 5), 15)
XCTAssertEqual(rules.setWinnerFromPoints(homePoints: 15, awayPoints: 13, currentSet: 5), .home)
}
func testMatchWinnerAfterThreeSets() {
let rules = MatchScoringContext(match: volleyMatch(setsToWin: 3))
XCTAssertEqual(rules.matchWinner(homeSets: 3, awaySets: 1), .home)
XCTAssertNil(rules.matchWinner(homeSets: 2, awaySets: 1))
}
func testCustomScoringRulesFromMatch() {
let custom = ScoringRules(pointsPerSet: 21, pointsDecidingSet: 15, minPointLead: 2)
let rules = MatchScoringContext(match: volleyMatch(setsToWin: 2, rules: custom))
XCTAssertEqual(rules.pointsTarget(currentSet: 1), 21)
XCTAssertEqual(rules.setWinnerFromPoints(homePoints: 21, awayPoints: 19, currentSet: 1), .home)
}
func testUsesSetLogicOnlyForVolleyAndRacket() {
XCTAssertTrue(MatchScoringContext(match: volleyMatch()).usesSetLogic)
var basket = volleyMatch()
basket = Match(
id: basket.id,
teamId: basket.teamId,
teamName: basket.teamName,
opponentName: basket.opponentName,
location: basket.location,
scheduledAt: basket.scheduledAt,
sportKey: "basket",
sportLabel: "Basket",
boardType: "basket",
overlayKind: "basket",
effectiveOverlayKind: "basket",
setsToWin: 0,
category: nil,
scoringRules: nil,
activeSessionId: nil,
activeSessionStatus: nil,
streamCompleted: false,
coachHubVisible: nil,
homePrimaryColor: nil,
homeSecondaryColor: nil,
homeLogoUrl: nil,
opponentPrimaryColor: nil,
opponentLogoUrl: nil
)
XCTAssertFalse(MatchScoringContext(match: basket).usesSetLogic)
}
func testDecodesFlexibleScorePayload() throws {
let json = """
{
"board_type": "basket",
"home_points": 42,
"away_points": 38,
"current_set": 1,
"period": "Q3",
"clock_secs": 312,
"clock_running": 1,
"data": { "home_score": 42, "away_score": 38, "period": 3, "overtime": false }
}
""".data(using: .utf8)!
let dto = try JSONDecoder().decode(ScoreStateDto.self, from: json)
let score = dto.toDomain()
XCTAssertEqual(score.boardType, "basket")
XCTAssertEqual(score.homePoints, 42)
XCTAssertEqual(score.periodLabel, "Q3")
XCTAssertEqual(score.clockSecs, 312)
XCTAssertTrue(score.clockRunning)
}
}
final class ScoreboardOverlayTests: XCTestCase {
func testScoreboardColumnsAfterSetClosed() {
let score = ScoreState(
homeSets: 1,
awaySets: 0,
homePoints: 0,
awayPoints: 0,
currentSet: 2,
setPartials: [SetPartial(set: 1, home: 26, away: 12)],
boardType: "volley"
)
let board = score.toScoreboardState(homeTeamName: "Casa", awayTeamName: "Ospite")
XCTAssertEqual(board.columns.count, 2)
XCTAssertEqual(board.columns[0].setNumber, 1)
XCTAssertEqual(board.columns[0].homePoints, 26)
XCTAssertEqual(board.columns[0].awayPoints, 12)
XCTAssertFalse(board.columns[0].isCurrent)
XCTAssertEqual(board.columns[1].setNumber, 2)
XCTAssertTrue(board.columns[1].isCurrent)
XCTAssertEqual(board.columns[1].homePoints, 0)
XCTAssertEqual(board.columns[1].awayPoints, 0)
}
}