169 lines
5.4 KiB
Dart
169 lines
5.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:timbre/playlists/playlists.dart';
|
|
import 'package:timbre/settings/settings_store.dart';
|
|
import 'package:timbre/subsonic/models.dart';
|
|
import 'package:timbre/subsonic/subsonic_client.dart';
|
|
|
|
/// Fake client so playlist mutations can be tested without a server. Methods
|
|
/// are overridden; the (unused) real Dio is created but never hit.
|
|
class _FakeClient extends SubsonicClient {
|
|
_FakeClient() : super(baseUrl: 'http://x', username: 'u', password: 'p');
|
|
|
|
final List<Playlist> playlists = [];
|
|
final Map<String, PlaylistDetail> details = {};
|
|
bool failNext = false;
|
|
int _seq = 0;
|
|
|
|
void _maybeThrow() {
|
|
if (failNext) {
|
|
failNext = false;
|
|
throw Exception('boom');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<Playlist>> getPlaylists() async => List.of(playlists);
|
|
|
|
@override
|
|
Future<PlaylistDetail> getPlaylist(String id) async =>
|
|
details[id] ?? PlaylistDetail(id: id, name: 'x');
|
|
|
|
@override
|
|
Future<PlaylistDetail?> createPlaylist(String name) async {
|
|
_maybeThrow();
|
|
final d = PlaylistDetail(id: 'pl-${_seq++}', name: name);
|
|
playlists.add(d.toSummary());
|
|
details[d.id] = d;
|
|
return d;
|
|
}
|
|
|
|
@override
|
|
Future<void> addTracksToPlaylist(String playlistId, List<String> songIds) async {
|
|
_maybeThrow();
|
|
}
|
|
|
|
@override
|
|
Future<void> removeTrackFromPlaylist(String playlistId, int index) async {
|
|
_maybeThrow();
|
|
}
|
|
|
|
@override
|
|
Future<void> renamePlaylist(String playlistId, String name) async {
|
|
_maybeThrow();
|
|
}
|
|
|
|
@override
|
|
Future<void> deletePlaylist(String id) async {
|
|
_maybeThrow();
|
|
}
|
|
}
|
|
|
|
Song _song(String id) => Song(id: id, title: 'Song $id');
|
|
|
|
void main() {
|
|
group('model round-trips', () {
|
|
test('Playlist toJson/fromJson', () {
|
|
final p = Playlist(id: 'p1', name: 'Mix', songCount: 3, duration: 600);
|
|
final back = Playlist.fromJson(p.toJson());
|
|
expect(back.id, 'p1');
|
|
expect(back.name, 'Mix');
|
|
expect(back.songCount, 3);
|
|
expect(back.duration, 600);
|
|
});
|
|
|
|
test('PlaylistDetail parses tracks from the `entry` key', () {
|
|
final detail = PlaylistDetail.fromJson({
|
|
'id': 'p1',
|
|
'name': 'Mix',
|
|
'entry': [
|
|
{'id': 's1', 'title': 'One'},
|
|
{'id': 's2', 'title': 'Two'},
|
|
],
|
|
});
|
|
expect(detail.songs.length, 2);
|
|
expect(detail.songs.first.id, 's1');
|
|
// toJson emits back under `entry` so the offline mirror round-trips.
|
|
expect(PlaylistDetail.fromJson(detail.toJson()).songs.length, 2);
|
|
});
|
|
});
|
|
|
|
group('AppSettings', () {
|
|
test('copyWith preserves format when the arg is omitted', () {
|
|
const s = AppSettings(downloadFormat: 'mp3');
|
|
expect(s.copyWith(streamMaxBitRate: 320).downloadFormat, 'mp3');
|
|
});
|
|
|
|
test('copyWith(downloadFormat: null) clears to Original', () {
|
|
const s = AppSettings(downloadFormat: 'mp3');
|
|
expect(s.copyWith(downloadFormat: null).downloadFormat, isNull);
|
|
});
|
|
|
|
test('toJson/fromJson round-trip', () {
|
|
const s = AppSettings(
|
|
streamMaxBitRate: 192, downloadMaxBitRate: 0, downloadFormat: 'opus');
|
|
final back = AppSettings.fromJson(s.toJson());
|
|
expect(back.streamMaxBitRate, 192);
|
|
expect(back.downloadMaxBitRate, 0);
|
|
expect(back.downloadFormat, 'opus');
|
|
});
|
|
});
|
|
|
|
group('PlaylistsController optimistic mutations', () {
|
|
late _FakeClient client;
|
|
late PlaylistsController controller;
|
|
|
|
setUp(() {
|
|
client = _FakeClient();
|
|
// Null server key => no filesystem access (persist/reload are no-ops).
|
|
controller = PlaylistsController(
|
|
clientGetter: () => client,
|
|
serverKeyGetter: () => null,
|
|
);
|
|
});
|
|
|
|
test('create adds a playlist and returns its id', () async {
|
|
final id = await controller.create('Roadtrip');
|
|
expect(id, isNotNull);
|
|
expect(controller.state.playlists.map((p) => p.name), contains('Roadtrip'));
|
|
expect(controller.state.details[id!]!.name, 'Roadtrip');
|
|
});
|
|
|
|
test('addTracks appends optimistically and bumps the count', () async {
|
|
final id = (await controller.create('Mix'))!;
|
|
await controller.addTracks(id, [_song('a'), _song('b')]);
|
|
expect(controller.state.details[id]!.songs.length, 2);
|
|
final summary = controller.state.playlists.firstWhere((p) => p.id == id);
|
|
expect(summary.songCount, 2);
|
|
});
|
|
|
|
test('removeAt drops the track optimistically', () async {
|
|
final id = (await controller.create('Mix'))!;
|
|
await controller.addTracks(id, [_song('a'), _song('b')]);
|
|
await controller.removeAt(id, 0);
|
|
final songs = controller.state.details[id]!.songs;
|
|
expect(songs.length, 1);
|
|
expect(songs.single.id, 'b');
|
|
});
|
|
|
|
test('rename updates the summary and cached detail', () async {
|
|
final id = (await controller.create('Old'))!;
|
|
await controller.rename(id, 'New');
|
|
expect(controller.state.playlists.single.name, 'New');
|
|
expect(controller.state.details[id]!.name, 'New');
|
|
});
|
|
|
|
test('delete removes the playlist', () async {
|
|
final id = (await controller.create('Temp'))!;
|
|
await controller.delete(id);
|
|
expect(controller.state.playlists, isEmpty);
|
|
expect(controller.state.details.containsKey(id), isFalse);
|
|
});
|
|
|
|
test('rename reverts when the server call fails', () async {
|
|
final id = (await controller.create('Keep'))!;
|
|
client.failNext = true;
|
|
await controller.rename(id, 'Nope');
|
|
expect(controller.state.playlists.single.name, 'Keep');
|
|
});
|
|
});
|
|
}
|