Files
minstrel/flutter_client/test/likes/like_button_test.dart
T
bvandeusen 66545bf8c3 feat(flutter): library screen with 5 tabs (Artists/Albums/History/Liked/Hidden)
Models:
- history_event.dart (HistoryEvent + HistoryPage envelope)
- quarantine_mine.dart (QuarantineMineRow for /api/quarantine/mine)
- Renamed Page<T> -> Paged<T> to avoid collision with Material's Page

Endpoints:
- library_lists.dart: listArtists / listAlbums (paged)
- me.dart: history + quarantineMine
- likes.dart: extended with listTracks/Albums/Artists (paged)

UI:
- library_screen.dart: TabBar over 5 tabs
- Artists tab: 3-col grid
- Albums tab: 2-col grid
- History tab: list with relative-time formatting (matches HistoryRow.svelte)
- Liked tab: 3 stacked sections (artists scroll-row, albums scroll-row, tracks list)
- Hidden tab: list of quarantined tracks with reason pill
- /library route + library_music icon on home AppBar

First page only for v1 (limit 50). Infinite scroll deferred.
2026-05-08 14:40:26 -04:00

81 lines
2.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/api/endpoints/likes.dart';
import 'package:minstrel/likes/like_button.dart';
import 'package:minstrel/likes/likes_provider.dart';
import 'package:minstrel/models/album.dart';
import 'package:minstrel/models/artist.dart';
import 'package:minstrel/models/page.dart';
import 'package:minstrel/models/track.dart';
import 'package:minstrel/theme/theme_data.dart';
class _ThrowingLikesApi implements LikesApi {
bool throwOnNext = false;
@override
Future<void> like(LikeKind kind, String id) async {
if (throwOnNext) throw StateError('boom');
}
@override
Future<void> unlike(LikeKind kind, String id) async {
if (throwOnNext) throw StateError('boom');
}
@override
Future<({Set<String> artists, Set<String> albums, Set<String> tracks})>
ids() async =>
(artists: <String>{}, albums: <String>{}, tracks: <String>{});
// The list-* methods aren't exercised by this test; return empty pages.
@override
Future<Paged<TrackRef>> listTracks({int limit = 50, int offset = 0}) async =>
const Paged<TrackRef>(items: [], total: 0, limit: 50, offset: 0);
@override
Future<Paged<AlbumRef>> listAlbums({int limit = 50, int offset = 0}) async =>
const Paged<AlbumRef>(items: [], total: 0, limit: 50, offset: 0);
@override
Future<Paged<ArtistRef>> listArtists({int limit = 50, int offset = 0}) async =>
const Paged<ArtistRef>(items: [], total: 0, limit: 50, offset: 0);
}
void main() {
testWidgets('tap toggles icon optimistically; rollback on error',
(tester) async {
final api = _ThrowingLikesApi();
final container = ProviderContainer(overrides: [
likesApiProvider.overrideWith((ref) async => api),
]);
addTearDown(container.dispose);
await tester.pumpWidget(UncontrolledProviderScope(
container: container,
child: MaterialApp(
theme: buildThemeData(),
home: const Scaffold(
body: LikeButton(kind: LikeKind.album, id: 'al-1'),
),
),
));
await tester.pumpAndSettle();
// First tap = like; succeeds → icon flips to favorite.
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(find.byIcon(Icons.favorite), findsOneWidget);
// Force the next mutation to fail; rollback restores prior state.
api.throwOnNext = true;
final notifier = container.read(likedIdsProvider.notifier);
try {
await notifier.toggle(LikeKind.album, 'al-1');
} catch (_) {/* expected */}
await tester.pump();
expect(find.byIcon(Icons.favorite), findsOneWidget); // rolled back
});
}