8fb6b4fbb3
Both fixes paired with the ci-flutter:3.44 rebuild that adds libsqlite3-dev to the runner image (CI-runner push #2: libsqlite3-0 → -dev because dart:ffi opens the unversioned .so symlink that only the dev package ships). widgets_smoke_test (TrackRow): TrackRow contains CachedIndicator which reaches audioCacheManagerProvider → appDbProvider → drift_flutter's `driftDatabase()`. That schedules a deferred-init Timer that outlives the test widget tree and trips the "A Timer is still pending after dispose" invariant. Override appDbProvider in the test to use AppDb(NativeDatabase.memory()) directly — bypasses drift_flutter's Timer-using init path, still exercises real SQLite via FFI. like_button_test (tap toggles + rollback): LikeButton was migrated to LucideHeart (SVG widget with `filled` bool) in the Lucide sweep; the test's `find.byIcon(Icons.favorite)` is stale. Replace with a small heartFilled() helper that reads the LucideHeart's `filled` prop straight off the widget tree. Same assertion semantics, just against the post-migration shape. The four sync_controller failures need no code change — they're the same root cause as the drift-tagged cohort (libsqlite3.so missing → try/catch returns null → `result?.upserts` is null instead of the expected 0). The image fix should clear them. 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.pump();
|
|
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.pump();
|
|
expect(heartFilled(), isTrue); // rolled back to liked
|
|
});
|
|
}
|