Zwei Schalter, die im selben Bildschirm untereinander stehen, ergaben zusammen gar keinen Aufmacher mehr: „Gesehenes aus Neuzugängen ausblenden" und eine angehakte Mediathek. Der Grund steht in `HomeFeed.prepared`. Der Zweig für das Ausblenden baut die betroffenen Reihen neu — und baute sie **ohne `libraryID`** neu, weil der neue Parameter dort nicht mitgegeben war. Getroffen hat es dabei ausgerechnet die Reihen `latest.<mediathek>`, die als einzige überhaupt eine tragen. Danach warf der Filter des Aufmachers jede Reihe ohne Mediathek weg, und übrig blieb nichts. Dasselbe an zweiter Stelle: `combiningContinueRows` verlor `libraryID` **und** `moreKey` — Letzteres ein eigener, älterer Fehler, durch den bei zusammengelegten Weiterschauen-Reihen „Alle anzeigen" wegfiel. **Dazu eine Regel, die ich zurücknehme.** Gestern stand hier: trifft die Auswahl nichts, bleibt nichts. Das war als Ehrlichkeit gedacht und ist in der Praxis der schlechtere Fehler — ein Aufmacher, der lautlos verschwindet, wird überall gesucht, nur nicht in einer Einstellung zwei Bildschirme weiter. Trifft die Auswahl keine einzige Reihe, gilt sie jetzt als wirkungslos. Das deckt auch den Fall ab, dass eine gespeicherte Kennung nach einem Serverwechsel ins Leere zeigt. Und damit solche Kennungen nicht unsichtbar weiterwirken, werden sie beim Öffnen des Bildschirms gegen die vorhandenen Mediatheken abgeglichen — vorher standen sie in keiner Zeile und liessen sich nicht abwählen. Geprüft im Simulator: „Gesehenes ausblenden" an, „Filme" angehakt — der Aufmacher ist da und zeigt einen Film. Vorher: nichts. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
7.4 KiB
Swift
179 lines
7.4 KiB
Swift
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"])
|
|
}
|
|
}
|