83a7099db3
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>
112 lines
4.2 KiB
Dart
112 lines
4.2 KiB
Dart
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;
|
|
|
|
@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();
|
|
// 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: <liked>), so the assertion
|
|
// is "the LucideHeart in the tree is filled" not "find an Icon."
|
|
bool heartFilled() =>
|
|
tester.widget<LucideHeart>(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
|
|
});
|
|
}
|