test(flutter): stub the transitive provider deps the drift cohort needs

Last 2 of the 6 surfaced drift-cohort failures. Diagnoses below.

quarantine_provider_test 'flag keeps drift optimistic + queues
mutation on server failure':
  MyQuarantineController.build() schedules unawaited _refreshFromServer()
  which reads connectivityProvider (a StreamProvider<bool>). In tests
  without an override, that stream never emits — its real impl listens
  to connectivity_plus's platform channel which has no fixture in
  flutter test. The success-path quarantine tests resolve their main
  flow before _refreshFromServer's chain reaches the connectivity read,
  so they don't trip the "disposed during loading" guard at tearDown.
  The throwing-API variant's `await ref.read(mutationQueueProvider)
  .enqueue(...)` advances enough async work that the connectivity read
  IS reached, then container.dispose() trips Riverpod's invariant.

  Fix: override connectivityProvider in the shared _container helper
  with `Stream.value(true)` so it resolves immediately. The previous
  `await Future.delayed(Duration.zero)` workaround is removed — the
  cleaner fix makes that band-aid unnecessary.

like_button_test 'tap toggles icon optimistically; rollback on error':
  LikesController.toggle:
    final user = _ref.read(authControllerProvider).value;
    if (user == null) return;
  The test only overrode `likesApiProvider`; authControllerProvider was
  in AsyncLoading state at toggle() time → user was null → early return
  → no drift write → likedIdsProvider never emits "liked" → heart never
  fills → assertion fails. The test would never have passed against the
  current controller; it was a stale survivor of an older API.

  Fix: stub authControllerProvider with _FakeAuthController that yields
  User(id: 'u1', …) immediately. Also add overrides for appDbProvider
  (NativeDatabase.memory, bypasses drift_flutter's Timer) and
  connectivityProvider (Stream.value(true), unblocks cacheFirst's
  isOnline check) — both are transitive dependencies of
  likedIdsProvider's cacheFirst.

After this push, the full drift cohort should be all-green. Fable #399
/ local #62 closes when CI lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 19:12:23 -04:00
parent a9e277eb13
commit 83a7099db3
2 changed files with 32 additions and 6 deletions
@@ -1,17 +1,31 @@
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<User?> build() async =>
const User(id: 'u1', username: 'tester', isAdmin: false);
}
class _ThrowingLikesApi implements LikesApi {
bool throwOnNext = false;
@@ -47,8 +61,19 @@ class _ThrowingLikesApi implements LikesApi {
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);
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/api/endpoints/quarantine.dart';
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/models/track.dart';
import 'package:minstrel/quarantine/quarantine_provider.dart';
@@ -43,6 +44,12 @@ ProviderContainer _container({required AppDb db, required QuarantineApi api}) {
return ProviderContainer(overrides: [
appDbProvider.overrideWithValue(db),
quarantineApiProvider.overrideWith((ref) async => api),
// _refreshFromServer() in build() reads connectivityProvider; in tests
// without this override it's a StreamProvider that never emits, so
// 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)),
]);
}
@@ -102,12 +109,6 @@ void main() {
expect(mutations, hasLength(1),
reason: 'failed flag should have been enqueued for replay');
expect(mutations.first.kind, 'quarantine.flag');
// Let in-flight drift watch() streams settle before tearDown
// disposes the container — otherwise Riverpod's StreamProvider
// fires "disposed during loading state" on the queued-mutation
// path (the success path resolves before the awaits above
// complete, so it doesn't need this).
await Future<void>.delayed(Duration.zero);
});
test('unflag deletes the drift row and calls the server',