import 'package:drift/native.dart' show NativeDatabase; 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/auth/auth_provider.dart' show authControllerProvider, AuthController; import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider; import 'package:minstrel/cache/connectivity_provider.dart'; import 'package:minstrel/cache/db.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/models/user.dart'; import 'package:minstrel/shared/widgets/lucide_heart.dart'; import 'package:minstrel/theme/theme_data.dart'; /// LikesController.toggle() early-returns if authControllerProvider has /// no user, so the test needs a stub controller that yields one. class _FakeAuthController extends AuthController { @override Future build() async => const User(id: 'u1', username: 'tester', isAdmin: false); } class _ThrowingLikesApi implements LikesApi { bool throwOnNext = false; @override Future like(LikeKind kind, String id) async { if (throwOnNext) throw StateError('boom'); } @override Future unlike(LikeKind kind, String id) async { if (throwOnNext) throw StateError('boom'); } @override Future<({Set artists, Set albums, Set tracks})> ids() async => (artists: {}, albums: {}, tracks: {}); // The list-* methods aren't exercised by this test; return empty pages. @override Future> listTracks({int limit = 50, int offset = 0}) async => const Paged(items: [], total: 0, limit: 50, offset: 0); @override Future> listAlbums({int limit = 50, int offset = 0}) async => const Paged(items: [], total: 0, limit: 50, offset: 0); @override Future> listArtists({int limit = 50, int offset = 0}) async => const Paged(items: [], total: 0, limit: 50, offset: 0); } void main() { testWidgets('tap toggles icon optimistically; rollback on error', (tester) async { final api = _ThrowingLikesApi(); // LikesController.toggle reaches authControllerProvider (for user.id), // appDbProvider (for drift), and likedIdsProvider (which transitively // reads connectivityProvider via cacheFirst). All four must be // stubbed for the optimistic-update path to actually run. final container = ProviderContainer(overrides: [ likesApiProvider.overrideWith((ref) async => api), authControllerProvider.overrideWith(_FakeAuthController.new), appDbProvider.overrideWith((ref) { final db = AppDb(NativeDatabase.memory()); ref.onDispose(db.close); return db; }), connectivityProvider.overrideWith((ref) => Stream.value(true)), ]); 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(); // Helper: read the current heart-fill state straight off the widget. // LikeButton renders LucideHeart(filled: ), so the assertion // is "the LucideHeart in the tree is filled" not "find an Icon." bool heartFilled() => tester.widget(find.byType(LucideHeart)).filled; // First tap = like; succeeds → heart flips to filled. await tester.tap(find.byType(IconButton)); await tester.pumpAndSettle(); expect(heartFilled(), isTrue); // Force the next mutation to fail; rollback restores prior state. api.throwOnNext = true; final controller = container.read(likesControllerProvider); try { await controller.toggle(LikeKind.album, 'al-1'); } catch (_) {/* expected */} await tester.pumpAndSettle(); expect(heartFilled(), isTrue); // rolled back to liked }); }