updates
This commit is contained in:
parent
d558aba246
commit
3bd713d667
17 changed files with 1566 additions and 132 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/widgets.dart' show Color;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:timbre/library/browse_query.dart';
|
||||
import 'package:timbre/playlists/playlists.dart';
|
||||
import 'package:timbre/settings/settings_store.dart';
|
||||
import 'package:timbre/state/providers.dart';
|
||||
|
|
@ -69,13 +70,14 @@ class _FakeClient extends SubsonicClient {
|
|||
}
|
||||
}
|
||||
|
||||
static Playlist _copy(Playlist p, {String? comment}) => Playlist(
|
||||
static Playlist _copy(Playlist p, {String? comment, bool? public}) =>
|
||||
Playlist(
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
songCount: p.songCount,
|
||||
duration: p.duration,
|
||||
owner: p.owner,
|
||||
public: p.public,
|
||||
public: public ?? p.public,
|
||||
coverArt: p.coverArt,
|
||||
comment: comment ?? p.comment,
|
||||
);
|
||||
|
|
@ -92,6 +94,13 @@ class _FakeClient extends SubsonicClient {
|
|||
songs: songs ?? d.songs,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> setPlaylistPublic(String playlistId, bool isPublic) async {
|
||||
_maybeThrow();
|
||||
final i = playlists.indexWhere((p) => p.id == playlistId);
|
||||
if (i >= 0) playlists[i] = _copy(playlists[i], public: isPublic);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrackFromPlaylist(String playlistId, int index) async {
|
||||
_maybeThrow();
|
||||
|
|
@ -163,6 +172,9 @@ void main() {
|
|||
staticAccentColor: Color(0xFF926CE9), // Purple
|
||||
defaultBrowseMode: BrowseMode.albums,
|
||||
searchMode: SearchMode.standard,
|
||||
maxConcurrentDownloads: 7,
|
||||
albumSort: AlbumSort.recentlyAdded,
|
||||
trackSort: TrackSort.yearDesc,
|
||||
nowPlayingCassette: true,
|
||||
);
|
||||
final back = AppSettings.fromJson(s.toJson());
|
||||
|
|
@ -170,6 +182,9 @@ void main() {
|
|||
expect(back.staticAccentColor, const Color(0xFF926CE9));
|
||||
expect(back.defaultBrowseMode, BrowseMode.albums);
|
||||
expect(back.searchMode, SearchMode.standard);
|
||||
expect(back.maxConcurrentDownloads, 7);
|
||||
expect(back.albumSort, AlbumSort.recentlyAdded);
|
||||
expect(back.trackSort, TrackSort.yearDesc);
|
||||
expect(back.nowPlayingCassette, isTrue);
|
||||
});
|
||||
|
||||
|
|
@ -181,6 +196,10 @@ void main() {
|
|||
expect(back.staticAccentColor, const Color(0xFFDF7E35)); // Orange
|
||||
expect(back.defaultBrowseMode, BrowseMode.artists);
|
||||
expect(back.searchMode, SearchMode.discovery);
|
||||
expect(back.maxConcurrentDownloads,
|
||||
AppSettings.defaultConcurrentDownloads);
|
||||
expect(back.albumSort, AlbumSort.nameAsc);
|
||||
expect(back.trackSort, TrackSort.titleAsc);
|
||||
expect(back.nowPlayingCassette, isFalse);
|
||||
// An unrecognised enum name also falls back rather than throwing.
|
||||
expect(
|
||||
|
|
@ -188,6 +207,13 @@ void main() {
|
|||
BrowseMode.artists,
|
||||
);
|
||||
});
|
||||
|
||||
test('fromJson clamps an out-of-range concurrent-download count', () {
|
||||
expect(AppSettings.fromJson({'maxConcurrentDownloads': 99})
|
||||
.maxConcurrentDownloads, AppSettings.maxConcurrentDownloadsCap);
|
||||
expect(AppSettings.fromJson({'maxConcurrentDownloads': 0})
|
||||
.maxConcurrentDownloads, AppSettings.minConcurrentDownloads);
|
||||
});
|
||||
});
|
||||
|
||||
group('SubsonicCredentials', () {
|
||||
|
|
@ -359,4 +385,153 @@ void main() {
|
|||
expect(controller.state.details[id]!.songs.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
group('Playlist sharing (TODO #5)', () {
|
||||
late _FakeClient client;
|
||||
late PlaylistsController controller;
|
||||
|
||||
setUp(() {
|
||||
client = _FakeClient();
|
||||
controller = PlaylistsController(
|
||||
clientGetter: () => client,
|
||||
serverKeyGetter: () => null,
|
||||
);
|
||||
});
|
||||
|
||||
test('isOwnedBy treats unknown owner/viewer as mine', () {
|
||||
expect(isOwnedBy(Playlist(id: 'a', name: 'A'), 'me'), isTrue);
|
||||
expect(
|
||||
isOwnedBy(Playlist(id: 'a', name: 'A', owner: 'bob'), null), isTrue);
|
||||
expect(
|
||||
isOwnedBy(Playlist(id: 'a', name: 'A', owner: 'me'), 'me'), isTrue);
|
||||
expect(
|
||||
isOwnedBy(Playlist(id: 'a', name: 'A', owner: 'bob'), 'me'), isFalse);
|
||||
});
|
||||
|
||||
test('setPublic flips the summary flag optimistically', () async {
|
||||
final id = (await controller.create('Mix'))!;
|
||||
await controller.setPublic(id, true);
|
||||
expect(controller.state.playlists.firstWhere((p) => p.id == id).public,
|
||||
isTrue);
|
||||
await controller.setPublic(id, false);
|
||||
expect(controller.state.playlists.firstWhere((p) => p.id == id).public,
|
||||
isFalse);
|
||||
});
|
||||
|
||||
test('setPublic reverts when the server call fails', () async {
|
||||
final id = (await controller.create('Mix'))!;
|
||||
client.failNext = true;
|
||||
await controller.setPublic(id, true);
|
||||
expect(controller.state.playlists.firstWhere((p) => p.id == id).public,
|
||||
isNot(true));
|
||||
});
|
||||
|
||||
test('saveCopy clones tracks into a new owned playlist', () async {
|
||||
final srcId = (await controller.create('Shared Mix'))!;
|
||||
await controller.addTracks(srcId, [_song('a'), _song('b'), _song('c')]);
|
||||
final source =
|
||||
controller.state.playlists.firstWhere((p) => p.id == srcId);
|
||||
|
||||
final copyId = await controller.saveCopy(source);
|
||||
expect(copyId, isNotNull);
|
||||
expect(copyId, isNot(srcId));
|
||||
// Two playlists named "Shared Mix"; the copy carries all three tracks.
|
||||
expect(controller.state.playlists.where((p) => p.name == 'Shared Mix'),
|
||||
hasLength(2));
|
||||
expect(controller.state.details[copyId]!.songs.map((s) => s.id),
|
||||
['a', 'b', 'c']);
|
||||
});
|
||||
});
|
||||
|
||||
group('browse filtering & sorting (TODO #2)', () {
|
||||
Album album(String id,
|
||||
{String? name, String? genre, int? year, String? created}) =>
|
||||
Album(id: id, name: name ?? id, genre: genre, year: year, created: created);
|
||||
Song song(String id,
|
||||
{String? title,
|
||||
String? artist,
|
||||
String? album,
|
||||
String? genre,
|
||||
int? year,
|
||||
String? created}) =>
|
||||
Song(
|
||||
id: id,
|
||||
title: title ?? id,
|
||||
artist: artist,
|
||||
album: album,
|
||||
genre: genre,
|
||||
year: year,
|
||||
created: created);
|
||||
|
||||
test('distinctGenres dedupes case-insensitively and sorts', () {
|
||||
final g = distinctGenres(['Rock', 'rock', 'Jazz', null, '', ' Pop ']);
|
||||
expect(g, ['Jazz', 'Pop', 'Rock']); // first-seen casing, trimmed
|
||||
});
|
||||
|
||||
test('distinctYears drops null/zero and sorts newest-first', () {
|
||||
expect(distinctYears([1999, null, 2020, 0, 1999]), [2020, 1999]);
|
||||
});
|
||||
|
||||
test('album genre filter is case-insensitive', () {
|
||||
final albums = [
|
||||
album('a', genre: 'Rock'),
|
||||
album('b', genre: 'jazz'),
|
||||
album('c', genre: 'ROCK'),
|
||||
];
|
||||
final out = applyAlbumQuery(
|
||||
albums, const BrowseFilter(genre: 'rock'), AlbumSort.nameAsc);
|
||||
expect(out.map((a) => a.id), ['a', 'c']);
|
||||
});
|
||||
|
||||
test('album year filter matches exactly', () {
|
||||
final albums = [album('a', year: 2001), album('b', year: 2002)];
|
||||
final out = applyAlbumQuery(
|
||||
albums, const BrowseFilter(year: 2002), AlbumSort.nameAsc);
|
||||
expect(out.map((a) => a.id), ['b']);
|
||||
});
|
||||
|
||||
test('album recentlyAdded sorts newest first, nulls last', () {
|
||||
final albums = [
|
||||
album('old', created: '2001-01-01T00:00:00'),
|
||||
album('new', created: '2020-01-01T00:00:00'),
|
||||
album('none'),
|
||||
];
|
||||
final out =
|
||||
applyAlbumQuery(albums, const BrowseFilter(), AlbumSort.recentlyAdded);
|
||||
expect(out.map((a) => a.id), ['new', 'old', 'none']);
|
||||
});
|
||||
|
||||
test('album yearDesc keeps null years last', () {
|
||||
final albums = [
|
||||
album('a', year: 1990),
|
||||
album('b'),
|
||||
album('c', year: 2010),
|
||||
];
|
||||
final out =
|
||||
applyAlbumQuery(albums, const BrowseFilter(), AlbumSort.yearDesc);
|
||||
expect(out.map((a) => a.id), ['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('track album sort falls back to track number then title', () {
|
||||
final songs = [
|
||||
song('s2', album: 'X', title: 'Zed'),
|
||||
song('s1', album: 'X', title: 'Abe'),
|
||||
];
|
||||
final out =
|
||||
applyTrackQuery(songs, const BrowseFilter(), TrackSort.albumAsc);
|
||||
// Same album, no track numbers → tie-break by title.
|
||||
expect(out.map((s) => s.id), ['s1', 's2']);
|
||||
});
|
||||
|
||||
test('track filter + sort compose', () {
|
||||
final songs = [
|
||||
song('a', genre: 'Rock', year: 2000, title: 'B'),
|
||||
song('b', genre: 'Rock', year: 2000, title: 'A'),
|
||||
song('c', genre: 'Jazz', year: 2000, title: 'C'),
|
||||
];
|
||||
final out = applyTrackQuery(
|
||||
songs, const BrowseFilter(genre: 'rock'), TrackSort.titleAsc);
|
||||
expect(out.map((s) => s.id), ['b', 'a']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue