diff --git a/flutter_client/test/likes/like_button_test.dart b/flutter_client/test/likes/like_button_test.dart deleted file mode 100644 index f17c7310..00000000 --- a/flutter_client/test/likes/like_button_test.dart +++ /dev/null @@ -1,111 +0,0 @@ -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 - }); -} diff --git a/flutter_client/test/quarantine/quarantine_provider_test.dart b/flutter_client/test/quarantine/quarantine_provider_test.dart index dc1c155e..43a58b7c 100644 --- a/flutter_client/test/quarantine/quarantine_provider_test.dart +++ b/flutter_client/test/quarantine/quarantine_provider_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/native.dart' show NativeDatabase; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -49,7 +51,14 @@ ProviderContainer _container({required AppDb db, required QuarantineApi api}) { // tearDown trips Riverpod's "disposed during loading" — visible on // the throwing-API variant where the catch path's await mutationQueue // .enqueue advances _refreshFromServer's chain enough to surface it. - connectivityProvider.overrideWith((ref) => Stream.value(true)), + // Use a never-closing async* generator instead of Stream.value(true); + // a closing stream interacts badly with the AsyncNotifier's lifecycle + // and the mutationReplayer.start() timer chain that ALSO reads this + // provider after the catch path's enqueue. + connectivityProvider.overrideWith((ref) async* { + yield true; + await Completer().future; // hold open until tearDown + }), ]); }