Files
plezy/test/services/music/music_queue_controller_test.dart
T

231 lines
7.6 KiB
Dart

import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/services/music/music_playback_service.dart';
import 'package:plezy/services/music/music_queue_controller.dart';
import '../../test_helpers/media_items.dart';
MediaItem _track(String id) =>
testMediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.track, title: 'Track $id', serverId: 'srv');
List<String> _ids(List<MediaItem> items) => [for (final i in items) i.id];
void main() {
final tracks = [for (var i = 0; i < 6; i++) _track('t$i')];
MusicQueueController controller({int seed = 42}) => MusicQueueController(random: Random(seed));
group('load', () {
test('starts at startIndex in canonical order', () {
final q = controller()..load(tracks, startIndex: 2);
expect(_ids(q.queue), ['t0', 't1', 't2', 't3', 't4', 't5']);
expect(q.cursor, 2);
expect(q.current!.id, 't2');
expect(q.shuffled, isFalse);
});
test('shuffle anchors the start track first', () {
final q = controller()..load(tracks, startIndex: 3, shuffle: true);
expect(q.shuffled, isTrue);
expect(q.cursor, 0);
expect(q.current!.id, 't3');
expect(_ids(q.queue).first, 't3');
expect(_ids(q.queue).toSet(), _ids(tracks).toSet());
});
test('empty load leaves an idle queue', () {
final q = controller()..load(const []);
expect(q.isEmpty, isTrue);
expect(q.cursor, -1);
expect(q.current, isNull);
expect(q.nextIndex(), isNull);
});
});
group('advancement (nextIndex/previousIndex)', () {
test('repeat off walks forward and ends after the last track', () {
final q = controller()..load(tracks, startIndex: 4);
expect(q.nextIndex(), 5);
q.jumpTo(5);
expect(q.nextIndex(), isNull);
expect(q.nextIndex(manual: true), isNull);
});
test('repeat all wraps both directions', () {
final q = controller()..load(tracks, startIndex: 5);
q.repeatMode = MusicRepeatMode.all;
expect(q.nextIndex(), 0);
q.jumpTo(0);
expect(q.previousIndex(), 5);
});
test('repeat one repeats naturally but steps on manual next', () {
final q = controller()..load(tracks, startIndex: 1);
q.repeatMode = MusicRepeatMode.one;
expect(q.nextIndex(), 1);
expect(q.nextIndex(manual: true), 2);
});
test('repeat one on the last track ends on manual next', () {
final q = controller()..load(tracks, startIndex: 5);
q.repeatMode = MusicRepeatMode.one;
expect(q.nextIndex(), 5);
expect(q.nextIndex(manual: true), isNull);
});
test('previousIndex steps back and stops at the head with repeat off', () {
final q = controller()..load(tracks, startIndex: 1);
expect(q.previousIndex(), 0);
q.jumpTo(0);
expect(q.previousIndex(), isNull);
});
});
group('jumpTo', () {
test('moves the cursor within bounds only', () {
final q = controller()..load(tracks);
q.jumpTo(4);
expect(q.current!.id, 't4');
q.jumpTo(99);
expect(q.cursor, 4);
q.jumpTo(-1);
expect(q.cursor, 4);
});
});
group('addNext / addToEnd', () {
test('addNext inserts directly after the current track', () {
final q = controller()..load(tracks, startIndex: 2);
q.addNext([_track('n1'), _track('n2')]);
expect(_ids(q.queue), ['t0', 't1', 't2', 'n1', 'n2', 't3', 't4', 't5']);
expect(q.current!.id, 't2');
});
test('addToEnd appends after everything', () {
final q = controller()..load(tracks, startIndex: 2);
q.addToEnd([_track('e1')]);
expect(_ids(q.queue).last, 'e1');
expect(q.current!.id, 't2');
});
test('added tracks survive an unshuffle in canonical order', () {
final q = controller()..load(tracks, startIndex: 0, shuffle: true);
q.addToEnd([_track('e1')]);
q.toggleShuffle(); // off — canonical = insertion order
expect(_ids(q.queue), ['t0', 't1', 't2', 't3', 't4', 't5', 'e1']);
});
});
group('removeAt', () {
test('before the cursor shifts the cursor back', () {
final q = controller()..load(tracks, startIndex: 3);
final wasCurrent = q.removeAt(1);
expect(wasCurrent, isFalse);
expect(q.current!.id, 't3');
expect(q.cursor, 2);
expect(_ids(q.queue), ['t0', 't2', 't3', 't4', 't5']);
});
test('after the cursor leaves the cursor alone', () {
final q = controller()..load(tracks, startIndex: 3);
expect(q.removeAt(5), isFalse);
expect(q.current!.id, 't3');
expect(q.cursor, 3);
});
test('at the cursor keeps the cursor on the following track', () {
final q = controller()..load(tracks, startIndex: 3);
expect(q.removeAt(3), isTrue);
expect(q.cursor, 3);
expect(q.current!.id, 't4');
});
test('at the cursor on the last track clamps back', () {
final q = controller()..load(tracks, startIndex: 5);
expect(q.removeAt(5), isTrue);
expect(q.cursor, 4);
expect(q.current!.id, 't4');
});
test('removing the only track empties the queue', () {
final q = controller()..load([_track('solo')]);
expect(q.removeAt(0), isTrue);
expect(q.isEmpty, isTrue);
expect(q.cursor, -1);
});
});
group('reorder (move)', () {
test('moving the current track moves the cursor with it', () {
final q = controller()..load(tracks, startIndex: 2);
q.move(2, 4);
expect(q.cursor, 4);
expect(q.current!.id, 't2');
expect(_ids(q.queue), ['t0', 't1', 't3', 't4', 't2', 't5']);
});
test('moving an entry across the cursor adjusts it', () {
final q = controller()..load(tracks, startIndex: 2);
q.move(0, 5);
expect(q.cursor, 1);
expect(q.current!.id, 't2');
q.move(5, 0);
expect(q.cursor, 2);
expect(q.current!.id, 't2');
});
test('moving entries on one side keeps the cursor', () {
final q = controller()..load(tracks, startIndex: 2);
q.move(3, 5);
expect(q.cursor, 2);
expect(q.current!.id, 't2');
});
});
group('toggleShuffle', () {
test('on: current track anchors first, rest shuffled after', () {
final q = controller()..load(tracks, startIndex: 2);
q.toggleShuffle();
expect(q.shuffled, isTrue);
expect(q.cursor, 0);
expect(q.current!.id, 't2');
expect(_ids(q.queue).toSet(), _ids(tracks).toSet());
});
test('off: canonical order restored, cursor follows current', () {
final q = controller()..load(tracks, startIndex: 2);
q.toggleShuffle();
q.jumpTo(3); // some shuffled position
final current = q.current!.id;
q.toggleShuffle();
expect(q.shuffled, isFalse);
expect(_ids(q.queue), ['t0', 't1', 't2', 't3', 't4', 't5']);
expect(q.current!.id, current);
});
});
group('clearUpcoming', () {
test('drops everything after the current track', () {
final q = controller()..load(tracks, startIndex: 2);
q.clearUpcoming();
expect(_ids(q.queue), ['t0', 't1', 't2']);
expect(q.current!.id, 't2');
expect(q.nextIndex(), isNull);
});
test('while shuffled also drops the canonical items', () {
final q = controller()..load(tracks, startIndex: 0, shuffle: true);
final kept = _ids(q.queue.sublist(0, 2));
q.jumpTo(1);
q.clearUpcoming();
expect(_ids(q.queue), kept);
q.toggleShuffle();
expect(_ids(q.queue).toSet(), kept.toSet());
expect(q.current!.id, kept[1]);
});
});
}