Files
Flaro/Packages/HaloKit/Tests/HaloDesignTests/MiniPlayerPlacementTests.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

102 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import Testing
@testable import HaloDesign
/// Wo die schwebende Videokachel landet.
@Suite("Kachel-Platzierung")
struct MiniPlayerPlacementTests {
/// Ein iPhone im Hochformat, Kachel 180 × 101.
private let placement = MiniPlayerPlacement(
bounds: CGSize(width: 390, height: 844),
tile: CGSize(width: 180, height: 101),
inset: 12
)
@Test("Die nähere Seite gewinnt")
func picksTheNearestSide() {
#expect(placement.nearestSide(toCenter: CGPoint(x: 50, y: 400)) == .leading)
#expect(placement.nearestSide(toCenter: CGPoint(x: 340, y: 400)) == .trailing)
}
@Test("Die Höhe bleibt frei, nicht auf Ecken festgelegt")
func keepsTheVerticalPosition() {
// Der Kern des Umbaus: vier Ecken sind schnell gebaut, aber im Weg.
let middle = placement.center(side: .trailing, verticalFraction: 0.5)
let top = placement.center(side: .trailing, verticalFraction: 0)
let bottom = placement.center(side: .trailing, verticalFraction: 1)
#expect(top.y < middle.y)
#expect(middle.y < bottom.y)
#expect(top.x == middle.x && middle.x == bottom.x)
}
@Test("Anteil und Mittelpunkt rechnen ineinander um")
func roundTripsTheFraction() {
for fraction in [0.0, 0.25, 0.5, 0.9, 1.0] {
let center = placement.center(side: .leading, verticalFraction: fraction)
let back = placement.verticalFraction(forCenter: center)
#expect(abs(back - fraction) < 0.0001)
}
}
@Test("Der Anteil bleibt zwischen null und eins")
func clampsTheFraction() {
// Sonst wandert die Kachel beim Drehen aus dem Bild: der Anteil wird
// gespeichert, und ein Wert über eins landete unterhalb des Randes.
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: -9_000)) == 0)
#expect(placement.verticalFraction(forCenter: CGPoint(x: 0, y: 9_000)) == 1)
#expect(placement.center(side: .leading, verticalFraction: 5).y <= 844 - 12)
}
@Test("An jeder Seite bleibt der Sicherheitsabstand")
func keepsTheInset() {
for side in MiniPlayerPlacement.Side.allCases {
for fraction in [0.0, 0.5, 1.0] {
let center = placement.center(side: side, verticalFraction: fraction)
#expect(center.x - 90 >= 12)
#expect(center.x + 90 <= 390 - 12)
#expect(center.y - 50.5 >= 12)
#expect(center.y + 50.5 <= 844 - 12)
}
}
}
@Test("Beim Ziehen rutscht nichts über den Rand")
func clampsWhileDragging() {
// Ohne diese Grenze zieht man die Kachel hinter den Bildschirmrand,
// und dort ist sie mit dem Finger nicht mehr einzuholen.
let farOut = placement.clamped(center: CGPoint(x: -500, y: -500))
#expect(farOut.x == 102.0)
#expect(farOut.y == 62.5)
let alsoFarOut = placement.clamped(center: CGPoint(x: 9_000, y: 9_000))
#expect(alsoFarOut.x == 288.0)
#expect(alsoFarOut.y == 781.5)
}
@Test("Ein Punkt in der Mitte bleibt, wo er ist")
func leavesValidPointsAlone() {
let inside = CGPoint(x: 195, y: 400)
#expect(placement.clamped(center: inside) == inside)
}
@Test("Ein zu enger Bereich schiebt die Kachel nicht hinaus")
func survivesTinyBounds() {
// Kommt beim Drehen und in der Übergangsphase vor: der Bereich ist
// kurzzeitig schmaler als die Kachel. Ein falsch herum geklammerter
// Wert schöbe sie dann dauerhaft aus dem Bild.
let narrow = MiniPlayerPlacement(
bounds: CGSize(width: 100, height: 80),
tile: CGSize(width: 180, height: 101),
inset: 12
)
#expect(narrow.clamped(center: CGPoint(x: 0, y: 0)) == CGPoint(x: 102, y: 62.5))
#expect(narrow.center(side: .trailing, verticalFraction: 1) == CGPoint(x: 102, y: 62.5))
}
}