325 lines
13 KiB
Dart
325 lines
13 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:melo_app/config/app_config.dart';
|
||
import 'package:melo_app/models/song.dart';
|
||
import 'package:melo_app/services/player_service.dart';
|
||
|
||
Song _song(int id, String titel) => Song(
|
||
id: id,
|
||
titel: titel,
|
||
kuenstler: 'Künstler $id',
|
||
dauerSekunden: 100,
|
||
dateiPfad: '',
|
||
);
|
||
|
||
void main() {
|
||
// PlayerService ist ein Singleton — Zustand vor jedem Test zurücksetzen
|
||
setUp(() {
|
||
final p = PlayerService();
|
||
p.setZufallsmodus(false);
|
||
p.setWiederholmodus(Wiederholmodus.aus);
|
||
p.cancelSleepTimer();
|
||
p.setWarteschlange([]);
|
||
// spiele() mit ungültiger Quelle loggt einen Fehler — ohne Netzwerk-Send
|
||
// (sonst versucht der Test einen echten HTTP-Post an den Log-Server).
|
||
AppConfig.sendeDiagnosedaten = false;
|
||
});
|
||
|
||
group('Warteschlange', () {
|
||
test('setWarteschlange + Getter', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
expect(p.warteschlange.length, 3);
|
||
expect(p.aktuellerIndex, 0);
|
||
expect(p.aktuellerSong?.titel, 'A');
|
||
});
|
||
|
||
test('warteschlange ist eine unveränderliche Kopie', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B')]);
|
||
expect(() => p.warteschlange.add(_song(3, 'C')), throwsUnsupportedError);
|
||
expect(p.warteschlange.length, 2);
|
||
});
|
||
|
||
test('spieleAlsNaechstes fügt nach dem aktuellen Song ein', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.spieleAlsNaechstes(_song(9, 'X'));
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 9, 2, 3]);
|
||
expect(p.aktuellerIndex, 0);
|
||
});
|
||
|
||
test('spieleAlsNaechstes ohne Warteschlange hängt an', () {
|
||
final p = PlayerService();
|
||
p.spieleAlsNaechstes(_song(1, 'A'));
|
||
expect(p.warteschlange.length, 1);
|
||
expect(p.aktuellerIndex, -1);
|
||
});
|
||
|
||
test('spieleAlsNaechstes verschiebt vorhandenen Song statt zu duplizieren', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.spieleAlsNaechstes(_song(3, 'C'));
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 3, 2]);
|
||
expect(p.warteschlange.length, 3);
|
||
// Wiederholter Aufruf erzeugt weiterhin keine Duplikate
|
||
p.spieleAlsNaechstes(_song(3, 'C'));
|
||
expect(p.warteschlange.length, 3);
|
||
});
|
||
|
||
test('spieleAlsNaechstes verschiebt Song von vor dem aktuellen', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')], startIndex: 2);
|
||
p.spieleAlsNaechstes(_song(1, 'A'));
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [2, 3, 1]);
|
||
expect(p.aktuellerIndex, 1); // aktueller Song (C) wandert auf Index 1
|
||
});
|
||
|
||
test('spieleAlsNaechstes im Shuffle: Song wird als nächstes gespielt', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C'), _song(4, 'D')]);
|
||
p.setZufallsmodus(true);
|
||
p.spieleAlsNaechstes(_song(3, 'C'));
|
||
// Kein Duplikat: alle 4 Songs genau einmal
|
||
expect(p.warteschlange.map((s) => s.id).toSet(), {1, 2, 3, 4});
|
||
expect(p.warteschlange.length, 4);
|
||
// C (jetzt direkt nach dem aktuellen) wird als nächstes gespielt
|
||
expect(p.naechsterIndex(), 1);
|
||
});
|
||
|
||
test('amEndeHinzufuegen hängt ans Ende an', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A')]);
|
||
p.amEndeHinzufuegen(_song(2, 'B'));
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 2]);
|
||
});
|
||
|
||
test('verschiebeInWarteschlange passt aktuellen Index an (Song danach)', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.verschiebeInWarteschlange(2, 0); // C nach vorne
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [3, 1, 2]);
|
||
expect(p.aktuellerIndex, 1); // aktuell A (war 0, ist jetzt 1)
|
||
});
|
||
|
||
test('verschiebeInWarteschlange passt aktuellen Index an (Song davor)', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.verschiebeInWarteschlange(0, 2); // aktueller Song selbst wandert
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [2, 3, 1]);
|
||
expect(p.aktuellerIndex, 2);
|
||
});
|
||
});
|
||
|
||
group('Repeat (Wiederholmodus)', () {
|
||
test('aus: naechsterIndex null am Ende der Warteschlange', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')], startIndex: 2);
|
||
p.setWiederholmodus(Wiederholmodus.aus);
|
||
expect(p.naechsterIndex(), isNull);
|
||
});
|
||
|
||
test('titel: immer der aktuelle Song', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')], startIndex: 1);
|
||
p.setWiederholmodus(Wiederholmodus.titel);
|
||
expect(p.naechsterIndex(), 1);
|
||
expect(p.naechsterIndex(), 1);
|
||
});
|
||
|
||
test('playlist: nach dem letzten Song wieder von vorn', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.setWiederholmodus(Wiederholmodus.playlist);
|
||
// Mittendrin: nächster ist der Folgesong
|
||
expect(p.naechsterIndex(), 1);
|
||
// Am Ende (letzter Song): wieder von vorn
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')], startIndex: 2);
|
||
expect(p.naechsterIndex(), 0);
|
||
});
|
||
});
|
||
|
||
group('Shuffle (Zufallsmodus)', () {
|
||
test('spielt alle anderen Songs genau einmal, ohne den aktuellen', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
final folge = <int>[];
|
||
int? idx;
|
||
while ((idx = p.naechsterIndex()) != null) {
|
||
folge.add(idx!);
|
||
if (folge.length > 10) break; // Schutz vor Endlosschleife
|
||
}
|
||
expect(folge.length, 4); // 5 Songs − aktueller
|
||
expect(folge.toSet(), {1, 2, 3, 4}); // keine Wiederholung, kein aktueller
|
||
});
|
||
|
||
test('shuffle + playlist: mischt nach Durchlauf neu weiter', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
p.setWiederholmodus(Wiederholmodus.playlist);
|
||
final folge = <int>[];
|
||
for (var i = 0; i < 12; i++) {
|
||
final idx = p.naechsterIndex();
|
||
expect(idx, isNotNull);
|
||
folge.add(idx!);
|
||
}
|
||
// Nie der aktuelle Song (Index 0), alle anderen kommen vor
|
||
expect(folge.every((i) => i >= 1 && i <= 4), isTrue);
|
||
expect(folge.toSet(), {1, 2, 3, 4});
|
||
});
|
||
|
||
test('shuffle mit einzelner Warteschlange: playlist wiederholt den Song', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A')]);
|
||
p.setZufallsmodus(true);
|
||
p.setWiederholmodus(Wiederholmodus.playlist);
|
||
expect(p.naechsterIndex(), 0);
|
||
expect(p.naechsterIndex(), 0);
|
||
});
|
||
|
||
test('shuffle + repeat aus + einzelne Warteschlange: Queue-Ende (null)', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A')]);
|
||
p.setZufallsmodus(true);
|
||
p.setWiederholmodus(Wiederholmodus.aus);
|
||
// Einzel-Song-Queue ohne Repeat darf denselben Song NICHT erneut liefern
|
||
expect(p.naechsterIndex(), isNull);
|
||
expect(p.naechsterIndex(), isNull);
|
||
});
|
||
|
||
test('shuffle schließt neuen aktuellen Index nach Umsortieren aus', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
p.verschiebeInWarteschlange(0, 2); // aktueller Song wandert auf Index 2
|
||
expect(p.aktuellerIndex, 2);
|
||
final folge = <int>[];
|
||
int? idx;
|
||
while ((idx = p.naechsterIndex()) != null) {
|
||
folge.add(idx!);
|
||
if (folge.length > 10) break;
|
||
}
|
||
expect(folge.toSet(), {0, 1, 3, 4}); // Index 2 (aktuell) nie dabei
|
||
});
|
||
});
|
||
|
||
group('Wiedergabe-Reihenfolge (Queue zeigt echte Reihenfolge)', () {
|
||
test('normal: aktueller Song zuerst, dann der Rest der Warteschlange', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange(
|
||
[_song(1, 'A'), _song(2, 'B'), _song(3, 'C'), _song(4, 'D')],
|
||
startIndex: 1);
|
||
// NICHT die Bibliotheks-Liste [1,2,3,4] — sondern ab dem aktuellen Song
|
||
expect(p.wiedergabeReihenfolge.map((s) => s.id).toList(), [2, 3, 4]);
|
||
});
|
||
|
||
test('shuffle: folgt der Zufallsreihenfolge ab der aktuellen Position', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
final vorher = p.wiedergabeReihenfolge;
|
||
expect(vorher.length, 5);
|
||
expect(vorher.first.id, 1); // aktueller Song zuerst
|
||
expect(vorher.map((s) => s.id).toSet(), {1, 2, 3, 4, 5});
|
||
// Der erste kommende Eintrag ist exakt der, den naechsterIndex() spielt
|
||
// (naechsterIndex liefert Queue-Indizes, daher über die Warteschlange vergleichen)
|
||
final naechster = p.naechsterIndex();
|
||
expect(p.warteschlange[naechster!].id, vorher[1].id);
|
||
});
|
||
|
||
test('shuffle nach Songwechsel: beginnt mit dem neuen aktuellen Song', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
final naechster = p.naechsterIndex()!;
|
||
p.spiele(p.warteschlange[naechster]); // Shuffle-Position wird synchronisiert
|
||
final reihenfolge = p.wiedergabeReihenfolge;
|
||
expect(reihenfolge.first.id, p.warteschlange[naechster].id);
|
||
// Der beim Mischen ausgeschlossene Song (Index 0 / id 1) taucht erst
|
||
// wieder auf, wenn er manuell gewählt wird — der Rest der Rotation
|
||
// (4 Songs) ist vollständig enthalten
|
||
expect(reihenfolge.length, 4);
|
||
expect(reihenfolge.map((s) => s.id).toSet(), {2, 3, 4, 5});
|
||
});
|
||
|
||
test('shuffle mit Einzel-Song-Queue: nur der aktuelle Song', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A')]);
|
||
p.setZufallsmodus(true);
|
||
expect(p.wiedergabeReihenfolge.map((s) => s.id).toList(), [1]);
|
||
});
|
||
|
||
test('verschieben (normal): ändert Wiedergabe-Reihenfolge und Queue', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange(
|
||
[_song(1, 'A'), _song(2, 'B'), _song(3, 'C'), _song(4, 'D')],
|
||
startIndex: 1);
|
||
p.verschiebeInWiedergabeReihenfolge(2, 1); // D vor C ziehen
|
||
expect(p.wiedergabeReihenfolge.map((s) => s.id).toList(), [2, 4, 3]);
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 2, 4, 3]);
|
||
expect(p.aktuellerIndex, 1); // aktueller Song bleibt an Position 0
|
||
});
|
||
|
||
test('verschieben (shuffle): ändert Zufallsreihenfolge, nicht die Queue', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([for (var i = 1; i <= 5; i++) _song(i, 'S$i')]);
|
||
p.setZufallsmodus(true);
|
||
final vorher = p.wiedergabeReihenfolge;
|
||
p.verschiebeInWiedergabeReihenfolge(4, 1); // letzten vor den ersten Kommenden
|
||
final nachher = p.wiedergabeReihenfolge;
|
||
expect(nachher.length, vorher.length);
|
||
expect(nachher.map((s) => s.id).toSet(), vorher.map((s) => s.id).toSet());
|
||
expect(nachher[1].id, vorher[4].id);
|
||
expect(nachher[2].id, vorher[1].id);
|
||
// Bibliotheks-Queue bleibt unangetastet (Shuffle-Liste ist die Wiedergabe)
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 2, 3, 4, 5]);
|
||
// Der verschobene Song wird wirklich als Nächstes gespielt
|
||
final naechster = p.naechsterIndex();
|
||
expect(p.warteschlange[naechster!].id, nachher[1].id);
|
||
});
|
||
|
||
test('Position 0 (aktueller Song) kann nicht verschoben werden', () {
|
||
final p = PlayerService();
|
||
p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]);
|
||
p.verschiebeInWiedergabeReihenfolge(0, 2); // von = aktueller Song → No-op
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 2, 3]);
|
||
// „Vor" den aktuellen Song ziehen landet auf Position 1 (danach)
|
||
p.verschiebeInWiedergabeReihenfolge(2, 0);
|
||
expect(p.warteschlange.map((s) => s.id).toList(), [1, 3, 2]);
|
||
});
|
||
|
||
test('leere Warteschlange: No-op', () {
|
||
final p = PlayerService();
|
||
p.verschiebeInWiedergabeReihenfolge(1, 2);
|
||
expect(p.wiedergabeReihenfolge, isEmpty);
|
||
});
|
||
});
|
||
|
||
group('Sleep-Timer', () {
|
||
test('setSleepTimer aktiviert + Restzeit vorhanden', () {
|
||
final p = PlayerService();
|
||
p.setSleepTimer(const Duration(minutes: 30));
|
||
expect(p.sleepTimerAktiv, isTrue);
|
||
expect(p.sleepRestzeit, isNotNull);
|
||
expect(p.sleepRestzeit!.inMinutes, greaterThanOrEqualTo(29));
|
||
});
|
||
|
||
test('cancelSleepTimer deaktiviert', () {
|
||
final p = PlayerService();
|
||
p.setSleepTimer(const Duration(minutes: 15));
|
||
p.cancelSleepTimer();
|
||
expect(p.sleepTimerAktiv, isFalse);
|
||
expect(p.sleepRestzeit, isNull);
|
||
});
|
||
|
||
test('setSleepTimer ersetzt einen laufenden Timer', () {
|
||
final p = PlayerService();
|
||
p.setSleepTimer(const Duration(minutes: 15));
|
||
p.setSleepTimer(const Duration(minutes: 60));
|
||
expect(p.sleepTimerAktiv, isTrue);
|
||
expect(p.sleepRestzeit!.inMinutes, greaterThanOrEqualTo(59));
|
||
});
|
||
});
|
||
}
|