From 8fb6b4fbb358efe0a0e9b8714399145bebb543e3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 20 May 2026 18:15:18 -0400 Subject: [PATCH] test(flutter): fix two latent failures exposed by un-skipping the drift cohort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../test/library/widgets_smoke_test.dart | 18 ++++++++++++++++-- .../test/likes/like_button_test.dart | 13 ++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/flutter_client/test/library/widgets_smoke_test.dart b/flutter_client/test/library/widgets_smoke_test.dart index 599dba58..56e39a8b 100644 --- a/flutter_client/test/library/widgets_smoke_test.dart +++ b/flutter_client/test/library/widgets_smoke_test.dart @@ -1,7 +1,10 @@ +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/cache/audio_cache_manager.dart' show appDbProvider; +import 'package:minstrel/cache/db.dart'; import 'package:minstrel/library/widgets/album_card.dart'; import 'package:minstrel/library/widgets/track_row.dart'; import 'package:minstrel/models/album.dart'; @@ -29,9 +32,20 @@ void main() { }); testWidgets('TrackRow shows mm:ss duration', (tester) async { - // TrackRow now contains CachedIndicator (ConsumerWidget) so a - // ProviderScope is required at the root. + // TrackRow contains CachedIndicator (ConsumerWidget) which reaches + // audioCacheManagerProvider → appDbProvider. Override appDbProvider + // with an explicit NativeDatabase.memory() executor — drift_flutter's + // default `driftDatabase()` schedules a deferred-init Timer that + // outlives the test widget tree and trips + // _verifyInvariants("A Timer is still pending after dispose"). await tester.pumpWidget(ProviderScope( + overrides: [ + appDbProvider.overrideWith((ref) { + final db = AppDb(NativeDatabase.memory()); + ref.onDispose(db.close); + return db; + }), + ], child: MaterialApp( theme: buildThemeData(), home: Scaffold( diff --git a/flutter_client/test/likes/like_button_test.dart b/flutter_client/test/likes/like_button_test.dart index 55e4a4ed..8976904d 100644 --- a/flutter_client/test/likes/like_button_test.dart +++ b/flutter_client/test/likes/like_button_test.dart @@ -9,6 +9,7 @@ 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 { @@ -62,10 +63,16 @@ void main() { )); await tester.pumpAndSettle(); - // First tap = like; succeeds → icon flips to favorite. + // 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.pump(); - expect(find.byIcon(Icons.favorite), findsOneWidget); + expect(heartFilled(), isTrue); // Force the next mutation to fail; rollback restores prior state. api.throwOnNext = true; @@ -74,6 +81,6 @@ void main() { await controller.toggle(LikeKind.album, 'al-1'); } catch (_) {/* expected */} await tester.pump(); - expect(find.byIcon(Icons.favorite), findsOneWidget); // rolled back + expect(heartFilled(), isTrue); // rolled back to liked }); }