import Foundation import Testing @testable import MediaKit @Suite("Auswahl für den Aufmacher") struct HeroSelectionTests { private func titel(_ id: String, backdrop: Bool = true) -> MediaItem { MediaItem(id: id, kind: .movie, title: "Film \(id)", art: backdrop ? "/art/\(id)" : nil) } private var zwanzig: [MediaItem] { (1...20).map { titel(String($0)) } } /// **Der Fehler, wegen dem es diesen Typ gibt.** Dieselbe Liste, dasselbe /// Zeitfenster, zweimal gefragt — und zweimal dasselbe. Vorher kam das /// nur heraus, solange die Liste sich nicht um einen einzigen Eintrag /// unterschied. @Test("Gleiche Eingabe, gleiches Ergebnis") func stableWithinWindow() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) #expect( HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) == HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) ) } /// Und der eigentliche Punkt: ein Titel mehr darf die übrigen nicht /// durcheinanderbringen. Genau das ist bei jedem Neustart passiert — /// „Weiterschauen" wächst und schrumpft ständig. @Test("Ein zusätzlicher Titel verschiebt die anderen nicht") func addingOneKeepsTheRest() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) let vorher = HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) let nachher = HeroSelection.ordered(zwanzig + [titel("99")], at: jetzt).map(\.id) // Der Neue darf irgendwo stehen; die übrigen müssen in derselben // Reihenfolge zueinander stehen wie vorher. #expect(nachher.filter { $0 != "99" } == vorher) } @Test("Ein fehlender Titel hinterlässt nur eine Lücke") func removingOneKeepsTheRest() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) let vorher = HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) let ohne = HeroSelection.ordered(zwanzig.filter { $0.id != "7" }, at: jetzt).map(\.id) #expect(ohne == vorher.filter { $0 != "7" }) } /// Sechs Stunden später eine andere Auswahl — sonst wäre es keine /// Abwechslung, sondern eine feste Reihenfolge. @Test("Im nächsten Zeitfenster steht es anders") func changesAcrossWindows() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) let später = jetzt.addingTimeInterval(7 * 3_600) #expect( HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) != HeroSelection.ordered(zwanzig, at: später).map(\.id) ) } @Test("Eine Stunde später steht es noch gleich") func stableAcrossAnHour() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) #expect( HeroSelection.ordered(zwanzig, at: jetzt).map(\.id) == HeroSelection.ordered(zwanzig, at: jetzt.addingTimeInterval(1_800)).map(\.id) ) } /// Ohne Hintergrundbild ist der Aufmacher eine graue Fläche mit Text. /// Ausschliessen wäre zu hart — schlechter losen ist genug. @Test("Titel ohne Hintergrundbild landen meistens hinten") func backdropsWin() { let jetzt = Date(timeIntervalSince1970: 1_800_000_000) let gemischt = (1...10).map { titel("mit\($0)") } + (1...10).map { titel("ohne\($0)", backdrop: false) } let ersteFünf = HeroSelection.ordered(gemischt, at: jetzt).prefix(5).map(\.id) #expect(ersteFünf.allSatisfy { $0.hasPrefix("mit") }) } /// Gäbe es nur Titel ohne Bild, dürfte nicht einfach nichts kommen. @Test("Gibt es nur Titel ohne Bild, kommen sie trotzdem") func withoutBackdropsStillReturns() { let ohne = (1...5).map { titel(String($0), backdrop: false) } #expect(HeroSelection.ordered(ohne).count == 5) } @Test("Eine leere Liste bleibt leer") func emptyStaysEmpty() { #expect(HeroSelection.ordered([]).isEmpty) } /// **Nicht `hashValue`.** Swift streut Hashes je Programmlauf zufällig — /// damit wäre die Auswahl nach jedem Start eine andere, also genau der /// Fehler, der hier behoben werden soll. @Test("Der Streuwert hängt nur am Text") func hashIsStable() { #expect(HeroSelection.stableHash("abc") == HeroSelection.stableHash("abc")) #expect(HeroSelection.stableHash("abc") != HeroSelection.stableHash("abd")) } @Test("Die Lose liegen zwischen null und eins") func lotsAreInRange() { for id in 1...200 { let los = HeroSelection.unitRandom(seed: UInt64(id)) #expect(los >= 0 && los < 1) } } } @Suite("Mediatheken für den Aufmacher") struct HeroLibraryFilterTests { private func reihe(_ id: String, library: String? = nil) -> MediaRowGroup { MediaRowGroup( id: id, title: id, libraryID: library, items: [MediaItem(id: "\(id).1", kind: .movie, title: "Titel")] ) } private var alle: [MediaRowGroup] { [reihe("resume"), reihe("latest.filme", library: "filme"), reihe("latest.serien", library: "serien"), reihe("favourites")] } /// **Der wichtigste Fall.** Wer nichts ankreuzt, meint „mir ist es /// gleich" — nicht „nimm nichts". Ein leerer Aufmacher wäre hier der /// Fehler, den niemand mit der Einstellung in Verbindung brächte. @Test("Ohne Auswahl bleibt alles") func emptySelectionKeepsEverything() { #expect(HeroSelection.rows(alle, limitedTo: []).map(\.id) == alle.map(\.id)) } @Test("Eine gewählte Mediathek lässt nur ihre Reihe durch") func selectionKeepsOnlyThatLibrary() { let übrig = HeroSelection.rows(alle, limitedTo: ["filme"]).map(\.id) #expect(übrig.contains("latest.filme")) #expect(!übrig.contains("latest.serien")) } /// **Die Regel, die zuerst andersherum stand.** Weiterschauen und /// Favoriten tragen keine Mediathek; sie trotzdem durchzulassen hiess, /// dass bei „nur Filme" weiter eine Serie oben stand — und niemand /// brachte das mit der Einstellung in Verbindung. @Test("Reihen ohne Mediathek fallen weg, sobald etwas gewählt ist") func rowsWithoutLibraryDropOutOnSelection() { let übrig = HeroSelection.rows(alle, limitedTo: ["filme"]).map(\.id) #expect(übrig == ["latest.filme"]) } /// **Der Fall, der den Aufmacher verschwinden liess.** Eine Mediathek, /// die es nicht mehr gibt — umbenannt, gelöscht, Server gewechselt —, /// oder ein Server, dessen Reihen gar keine Mediathek nennen. Bliebe es /// bei „nichts übrig", stünde oben nichts mehr, und niemand brächte das /// mit einer Einstellung zwei Bildschirme weiter in Verbindung. @Test("Trifft die Auswahl nichts, gilt sie als wirkungslos") func unknownSelectionFallsBack() { #expect(HeroSelection.rows(alle, limitedTo: ["gibtsnicht"]).map(\.id) == alle.map(\.id)) } /// Reihen ohne Mediathek allein sind kein Rückfall: gibt es daneben /// passende, bleiben nur die. Sonst wäre die Ausnahme oben die Regel. @Test("Der Rückfall greift nur, wenn wirklich nichts passt") func fallbackOnlyWhenEmpty() { let übrig = HeroSelection.rows(alle, limitedTo: ["serien"]).map(\.id) #expect(übrig == ["latest.serien"]) } @Test("Mehrere gewählte Mediatheken kommen alle durch") func multipleSelection() { let übrig = HeroSelection.rows(alle, limitedTo: ["filme", "serien"]).map(\.id) #expect(übrig == ["latest.filme", "latest.serien"]) } }