Files
Flaro/Packages/HaloKit/Tests/HaloPlaybackTests/TrackMenuTests.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit,
damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind.

712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25
Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit
`HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`,
`JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben.

`docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind
ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben
hiesse, die Vergangenheit zu glätten.

**Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis:
`com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte
Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container;
Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu
laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen.

**Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der
Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr
heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter
`…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur
anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim
Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor
dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet.

Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht
statt umbenannt — sie steht ohnehin nicht im Repo.

Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist
ein Handgriff ausserhalb des Projekts.

368 Tests grün. iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:12:32 +02:00

116 lines
4.5 KiB
Swift

import Testing
import MediaKit
@testable import HaloPlayback
@Suite("Was im Spurenmenü steht")
struct TrackMenuTests {
private func spur(
_ id: String,
kind: MediaStream.Kind = .audio,
sprache: String? = "Deutsch",
code: String? = "deu",
name: String? = nil,
codec: String? = "AC3"
) -> MediaStream {
MediaStream(
id: id, kind: kind, codec: codec, language: sprache, languageCode: code,
displayTitle: name, channels: nil, isSelected: false, isForced: false,
title: nil, isDefault: false, isHearingImpaired: false, isOriginal: false
)
}
// MARK: - Wann es einen Neuaufbau kostet
/// **Der Fall, der bisher nirgends stand.** Rechnet der Server um, liegt
/// im Stream genau eine Tonspur — jede andere Wahl verlangt eine neue
/// Bestellung und damit Sekunden mit schwarzem Bild.
@Test("Beim Umrechnen kostet jede Wahl einen Neuaufbau")
func transcodeAlwaysRebuilds() {
let einträge = TrackMenu.audio(
from: [spur("1"), spur("2")],
selected: nil, isDirectPlay: false, playerTrackCount: 1
)
#expect(einträge.allSatisfy { $0.rebuilds })
}
@Test("Unverändert gespielt und gleiche Anzahl: kein Neuaufbau")
func directPlayMatchingCountIsCheap() {
let einträge = TrackMenu.audio(
from: [spur("1"), spur("2")],
selected: nil, isDirectPlay: true, playerTrackCount: 2
)
#expect(einträge.allSatisfy { !$0.rebuilds })
}
/// **Die Regel aus `PlayerModel`, zum ersten Mal prüfbar.** Die Zuordnung
/// geht über die Reihenfolge; stimmt die Anzahl nicht, ist die Annahme
/// falsch, und es wäre die falsche Sprache statt einer Unterbrechung.
/// Auslösen liess sich das bisher nur an einem echten Server.
@Test("Abweichende Anzahl heißt Neuaufbau, auch unverändert gespielt")
func directPlayMismatchRebuilds() {
let einträge = TrackMenu.audio(
from: [spur("1"), spur("2"), spur("3")],
selected: nil, isDirectPlay: true, playerTrackCount: 2
)
#expect(einträge.allSatisfy { $0.rebuilds })
}
// MARK: - Untertitel
@Test("„Aus“ steht vorn und kostet nie einen Neuaufbau")
func offComesFirstAndIsFree() {
let einträge = TrackMenu.subtitles(
from: [spur("1", kind: .subtitle)],
selected: nil, isDirectPlay: false, offLabel: "Aus"
)
#expect(einträge.first?.id == nil)
#expect(einträge.first?.title == "Aus")
#expect(einträge.first?.isSelected == true)
#expect(einträge.first?.rebuilds == false)
}
/// Nur was der Player wirklich hat, lässt sich ohne Neuaufbau schalten.
@Test("Eine Untertitelspur, die der Player nicht hat, kostet einen Neuaufbau")
func unmappableSubtitleRebuilds() {
let deutsch = spur("1", kind: .subtitle, sprache: "Deutsch", code: "deu")
let englisch = spur("2", kind: .subtitle, sprache: "English", code: "eng")
let einträge = TrackMenu.subtitles(
from: [deutsch, englisch],
selected: nil, isDirectPlay: true,
playerTrackLanguages: ["de"], offLabel: "Aus"
)
// „deu" und „de" sind dieselbe Sprache — das entscheidet LanguageCode.
#expect(einträge[1].rebuilds == false)
#expect(einträge[2].rebuilds == true)
}
// MARK: - Beschriftung
@Test("Der Name des Servers gewinnt, sonst Sprache und Codec")
func fallsBackToLanguageAndCodec() {
let mitNamen = spur("1", name: "Deutsch (AC3 5.1)")
let ohneNamen = spur("2", sprache: "English", codec: "eac3")
let einträge = TrackMenu.audio(
from: [mitNamen, ohneNamen],
selected: mitNamen, isDirectPlay: true, playerTrackCount: 2
)
#expect(einträge[0].title == "Deutsch (AC3 5.1)")
#expect(einträge[1].title == "English · EAC3")
#expect(einträge[0].isSelected)
#expect(!einträge[1].isSelected)
}
/// Ohne alles bleibt ein Strich — besser als eine leere Zeile, bei der
/// niemand weiss, ob sie anklickbar ist.
@Test("Ohne Sprache und Codec bleibt ein Strich")
func namelessStreamStillHasALabel() {
let leer = spur("7", sprache: nil, code: nil, codec: nil)
let einträge = TrackMenu.audio(
from: [leer], selected: nil, isDirectPlay: true, playerTrackCount: 1
)
#expect(einträge[0].title == "—")
#expect(einträge[0].id == 7)
}
}