updates
This commit is contained in:
parent
981b4836f9
commit
ed910748cb
34 changed files with 2054 additions and 153 deletions
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/widgets.dart' show Color;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:timbre/playlists/playlists.dart';
|
||||
import 'package:timbre/settings/settings_store.dart';
|
||||
import 'package:timbre/state/providers.dart';
|
||||
import 'package:timbre/subsonic/credentials.dart';
|
||||
import 'package:timbre/subsonic/models.dart';
|
||||
import 'package:timbre/subsonic/subsonic_client.dart';
|
||||
|
||||
|
|
@ -12,6 +15,9 @@ class _FakeClient extends SubsonicClient {
|
|||
final List<Playlist> playlists = [];
|
||||
final Map<String, PlaylistDetail> details = {};
|
||||
bool failNext = false;
|
||||
// Fails only the `setPlaylistComment` call — lets a test drive the tag
|
||||
// orphan-cleanup path (create succeeds, marker stamp fails).
|
||||
bool failComment = false;
|
||||
int _seq = 0;
|
||||
|
||||
void _maybeThrow() {
|
||||
|
|
@ -37,11 +43,55 @@ class _FakeClient extends SubsonicClient {
|
|||
return d;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setPlaylistComment(String playlistId, String comment) async {
|
||||
if (failComment) {
|
||||
failComment = false;
|
||||
throw Exception('comment failed');
|
||||
}
|
||||
final i = playlists.indexWhere((p) => p.id == playlistId);
|
||||
if (i >= 0) playlists[i] = _copy(playlists[i], comment: comment);
|
||||
final d = details[playlistId];
|
||||
if (d != null) details[playlistId] = _copyDetail(d, comment: comment);
|
||||
}
|
||||
|
||||
// The server persists appends, so getPlaylist reflects real membership — the
|
||||
// fake must too, or `addToTag`'s de-dupe check has nothing to read.
|
||||
@override
|
||||
Future<void> addTracksToPlaylist(String playlistId, List<String> songIds) async {
|
||||
_maybeThrow();
|
||||
final d = details[playlistId];
|
||||
if (d != null) {
|
||||
details[playlistId] = _copyDetail(d, songs: [
|
||||
...d.songs,
|
||||
for (final sid in songIds) Song(id: sid, title: 'Song $sid'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static Playlist _copy(Playlist p, {String? comment}) => Playlist(
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
songCount: p.songCount,
|
||||
duration: p.duration,
|
||||
owner: p.owner,
|
||||
public: p.public,
|
||||
coverArt: p.coverArt,
|
||||
comment: comment ?? p.comment,
|
||||
);
|
||||
|
||||
static PlaylistDetail _copyDetail(PlaylistDetail d,
|
||||
{String? comment, List<Song>? songs}) =>
|
||||
PlaylistDetail(
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
songCount: d.songCount,
|
||||
duration: d.duration,
|
||||
coverArt: d.coverArt,
|
||||
comment: comment ?? d.comment,
|
||||
songs: songs ?? d.songs,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> removeTrackFromPlaylist(String playlistId, int index) async {
|
||||
_maybeThrow();
|
||||
|
|
@ -106,6 +156,101 @@ void main() {
|
|||
expect(back.downloadMaxBitRate, 0);
|
||||
expect(back.downloadFormat, 'opus');
|
||||
});
|
||||
|
||||
test('toJson/fromJson round-trip preserves the new preference fields', () {
|
||||
const s = AppSettings(
|
||||
useStaticAccent: true,
|
||||
staticAccentColor: Color(0xFF926CE9), // Purple
|
||||
defaultBrowseMode: BrowseMode.albums,
|
||||
searchMode: SearchMode.standard,
|
||||
nowPlayingCassette: true,
|
||||
);
|
||||
final back = AppSettings.fromJson(s.toJson());
|
||||
expect(back.useStaticAccent, isTrue);
|
||||
expect(back.staticAccentColor, const Color(0xFF926CE9));
|
||||
expect(back.defaultBrowseMode, BrowseMode.albums);
|
||||
expect(back.searchMode, SearchMode.standard);
|
||||
expect(back.nowPlayingCassette, isTrue);
|
||||
});
|
||||
|
||||
test('fromJson falls back to defaults for missing/unknown values', () {
|
||||
// An older settings.json without the new keys loads with safe defaults.
|
||||
final back = AppSettings.fromJson({'streamMaxBitRate': 128});
|
||||
expect(back.streamMaxBitRate, 128);
|
||||
expect(back.useStaticAccent, isFalse);
|
||||
expect(back.staticAccentColor, const Color(0xFFDF7E35)); // Orange
|
||||
expect(back.defaultBrowseMode, BrowseMode.artists);
|
||||
expect(back.searchMode, SearchMode.discovery);
|
||||
expect(back.nowPlayingCassette, isFalse);
|
||||
// An unrecognised enum name also falls back rather than throwing.
|
||||
expect(
|
||||
AppSettings.fromJson({'defaultBrowseMode': 'bogus'}).defaultBrowseMode,
|
||||
BrowseMode.artists,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('SubsonicCredentials', () {
|
||||
test('toJson/fromJson round-trip', () {
|
||||
const c = SubsonicCredentials(
|
||||
url: 'https://x', username: 'u', password: 'p', alias: 'Home');
|
||||
final back = SubsonicCredentials.fromJson(c.toJson());
|
||||
expect(back.url, 'https://x');
|
||||
expect(back.username, 'u');
|
||||
expect(back.password, 'p');
|
||||
expect(back.alias, 'Home');
|
||||
});
|
||||
|
||||
test('id ignores a trailing slash so the same server maps to one key', () {
|
||||
const a =
|
||||
SubsonicCredentials(url: 'https://x/', username: 'u', password: 'p');
|
||||
const b =
|
||||
SubsonicCredentials(url: 'https://x', username: 'u', password: 'q');
|
||||
expect(a.id, b.id);
|
||||
});
|
||||
|
||||
test('display prefers a non-empty alias, else the host', () {
|
||||
expect(
|
||||
const SubsonicCredentials(url: 'https://x', username: 'u', password: 'p')
|
||||
.display,
|
||||
'x',
|
||||
);
|
||||
expect(
|
||||
const SubsonicCredentials(
|
||||
url: 'https://x', username: 'u', password: 'p', alias: 'Home')
|
||||
.display,
|
||||
'Home',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('filterSearchToStandard (TODO #4)', () {
|
||||
test('keeps name/title matches and drops artist-only song matches', () {
|
||||
final result = SearchResult3(
|
||||
artists: [Artist(id: 'a1', name: 'Chard')],
|
||||
albums: [
|
||||
Album(id: 'al1', name: 'Cha Cha'),
|
||||
Album(id: 'al2', name: 'Nope'),
|
||||
],
|
||||
songs: [
|
||||
Song(id: 's1', title: 'Cha times', artist: 'Zed'),
|
||||
Song(id: 's2', title: 'Other', artist: 'Cha Band'), // artist-only
|
||||
],
|
||||
);
|
||||
final filtered = filterSearchToStandard(result, 'Cha');
|
||||
expect(filtered.artists.map((a) => a.id), ['a1']);
|
||||
expect(filtered.albums.map((a) => a.id), ['al1']);
|
||||
expect(filtered.songs.map((s) => s.id), ['s1']); // s2 dropped
|
||||
});
|
||||
|
||||
test('is case-insensitive', () {
|
||||
final result = SearchResult3(
|
||||
artists: const [],
|
||||
albums: const [],
|
||||
songs: [Song(id: 's1', title: 'CHA')],
|
||||
);
|
||||
expect(filterSearchToStandard(result, 'cha').songs.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('PlaylistsController optimistic mutations', () {
|
||||
|
|
@ -166,4 +311,52 @@ void main() {
|
|||
expect(controller.state.playlists.single.name, 'Keep');
|
||||
});
|
||||
});
|
||||
|
||||
group('Tags (playlists marked with the tag comment)', () {
|
||||
late _FakeClient client;
|
||||
late PlaylistsController controller;
|
||||
|
||||
setUp(() {
|
||||
client = _FakeClient();
|
||||
controller = PlaylistsController(
|
||||
clientGetter: () => client,
|
||||
serverKeyGetter: () => null,
|
||||
);
|
||||
});
|
||||
|
||||
test('createTag stamps the marker so the playlist reads as a tag', () async {
|
||||
final id = (await controller.createTag('cooking'))!;
|
||||
final summary = controller.state.playlists.single;
|
||||
expect(summary.id, id);
|
||||
expect(summary.comment, kTagMarker);
|
||||
expect(isTagPlaylist(summary), isTrue);
|
||||
});
|
||||
|
||||
test('createTag is unique per name (case-insensitive)', () async {
|
||||
final first = await controller.createTag('Cooking');
|
||||
final second = await controller.createTag('cooking');
|
||||
expect(second, first); // reused, not duplicated
|
||||
expect(controller.state.playlists.length, 1);
|
||||
});
|
||||
|
||||
test('createTag deletes the orphan if the marker stamp fails', () async {
|
||||
client.failComment = true;
|
||||
final id = await controller.createTag('running');
|
||||
expect(id, isNull);
|
||||
// No unmarked, nameless playlist left behind.
|
||||
expect(controller.state.playlists, isEmpty);
|
||||
});
|
||||
|
||||
test('addToTag skips songs already carrying the tag', () async {
|
||||
final id = (await controller.createTag('run'))!;
|
||||
await controller.addToTag(id, [_song('a'), _song('b')]);
|
||||
await controller.addToTag(id, [_song('b'), _song('c')]); // b duplicate
|
||||
final ids = controller.state.details[id]!.songs.map((s) => s.id).toList();
|
||||
expect(ids, ['a', 'b', 'c']);
|
||||
|
||||
// Re-adding an existing member is a no-op.
|
||||
await controller.addToTag(id, [_song('a')]);
|
||||
expect(controller.state.details[id]!.songs.length, 3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue