a9e277eb13
All six tests had been silently skipped under @Tags(['drift']) for 6+ months, so they accumulated test-vs-implementation drift. Now running against the libsqlite3-bearing ci-flutter:3.44 image, each failed for a distinct reason. Diagnoses below. audio_cache_manager_test 'usageBytes sums sizeBytes across rows': usageBytes() is a directory walk (authoritative on-disk total, catches orphan partials the index misses). The test inserted drift rows but never wrote files, so the walk returned 0. The actual API for summing drift sizeBytes is bucketUsage(). Rename to 'bucketUsage sums drift sizeBytes across rows', use that API, assert liked+rolling == 350. Also give the two rows unique paths per row-shape sanity. sync_controller_test (3 200-path tests, all returning null result): Map literals in Dart 3 with mixed value types infer as Map<String, Object>, not Map<String, dynamic>. The sync controller casts `resp.data as Map<String, dynamic>` (and several nested casts), which is invariant on generics and throws TypeError. The silent try/catch in sync() swallowed the throw and returned null. Real JSON parsing produces Map<String, dynamic>, so this never surfaced in production. Fix: route the test stub body through jsonDecode(jsonEncode(body)) in _stubDio — mimics real Dio's parsed-response shape. Affects '200 with artist upsert', '200 with track delete', and 'like_track upsert + delete round-trip'. quarantine_provider_test 'flag keeps drift optimistic + queues mutation on server failure': When the API stub throws, the controller catches + queues to CachedMutations. The drift watch() stream in MyQuarantineController was still in loading state when addTearDown disposed the container, tripping Riverpod's "StreamProvider disposed during loading" assertion. The success-path tests resolved before tearDown so they didn't see it. Fix: await one microtask before the test ends so the stream emits. like_button_test 'tap toggles icon optimistically; rollback on error': After the LikeButton was migrated to LucideHeart in the Lucide sweep, the prior fix replaced the find.byIcon assertion with a heartFilled() helper reading LucideHeart.filled. But likesController .toggle() goes through an async chain (optimistic state flip + await api.like + state notification), which one frame of tester.pump() doesn't flush. Use pumpAndSettle after both tap and rollback toggle so the widget rebuilds with the new state before the assertion. After this push, the drift cohort should be all-green on the libsqlite3-bearing image. Fable #399 / local #62. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.0 KiB
Dart
87 lines
3.0 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/shared/widgets/lucide_heart.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();
|
|
|
|
// 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
|
|
});
|
|
}
|