import 'package:drift/drift.dart' show Value, driftRuntimeOptions; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:melo/library/category_service.dart'; import 'package:melo/library/database.dart'; import 'package:melo/library/playlist_service.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/melo_cloud_service.dart'; import 'package:melo/services/sync_service.dart'; import 'package:melo/settings/app_settings.dart'; import 'package:melo/shared/sort_store.dart'; import 'package:melo/shared/sortable_song_list.dart'; class _MemorySpeicher implements TokenSpeicher { _MemorySpeicher(this.werte); final Map werte; @override Future lesen(String key) async => werte[key]; @override Future schreiben(String key, String wert) async => werte[key] = wert; @override Future loeschen(String key) async => werte.remove(key); } /// Merkt sich nur, was hochgeladen werden sollte. Der echte Upload braucht /// Dateien und einen Server — hier geht es um den Weg vom Knopf zum Dienst. class _FakeSync extends SyncService { _FakeSync(MeloDb db) : super( db: db, cloud: MeloCloudService( auth: BakaAuth(speicher: _MemorySpeicher({})), client: MockClient( (_) async => http.Response('{"status":"ok"}', 200)), ), ); final hochgeladen = []; @override Future ladeAusgewaehlteHoch(List songs) async { hochgeladen.addAll([for (final s in songs) s.id]); return const UploadErgebnis(hochgeladen: 1); } } /// „Auf den Server laden" gehört in „Meine Musik" — und **nur** dorthin. /// [SortableSongList] wird in fünf Ansichten benutzt; ohne Scoping erschiene /// die Aktion auch bei Favoriten, Wiedergabelisten und Titellisten. /// /// Aufbau bewusst im Testkörper, nicht in `setUp` (siehe auswahl_modus_test). void main() { final lieder = [ for (var i = 0; i < 3; i++) Song( id: 'song-$i', path: '/music/$i.mp3', title: 'Titel $i', artist: 'Neoni', dateAddedMs: i, updatedAtMs: 0, deleted: false, playCount: 0, categoriesEdited: false, metadataEdited: false, ), ]; Future beruhige(WidgetTester tester) => tester.pumpAndSettle( const Duration(milliseconds: 100), EnginePhase.sendSemanticsUpdate, const Duration(seconds: 5), ); /// Räumt den Widget-Baum kontrolliert ab, statt es dem Testrahmen am /// Testende zu überlassen: sonst bleibt ein von drift/SnackBar beim /// Aufräumen geplanter Timer offen (siehe auswahl_modus_test.dart / /// BACKLOG.md — derselbe Hänger, dieselbe Ursache). Future aufraeumen(WidgetTester tester) async { await tester.pumpWidget(const SizedBox()); await tester.pump(const Duration(milliseconds: 1)); } Future<_FakeSync> pumpe(WidgetTester tester, {required bool serverUpload}) async { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; SharedPreferences.setMockInitialValues({}); final db = MeloDb(NativeDatabase.memory()); // Kein db.close(): drift plant dabei eine Aufräum-Aufgabe (Timer) ein, // die der Testrahmen nicht mehr abarbeitet (siehe BACKLOG.md und // auswahl_modus_test.dart — derselbe Hänger, dieselbe Ursache). final sync = _FakeSync(db); final einstellungen = AppSettings(); await einstellungen.init(); for (final song in lieder) { await db.into(db.songs).insert(SongsCompanion.insert( id: song.id, path: song.path, title: song.title, artist: Value(song.artist), dateAddedMs: song.dateAddedMs, updatedAtMs: 0, )); } await tester.pumpWidget( MultiProvider( providers: [ Provider.value(value: db), ChangeNotifierProvider.value( value: CategoryService(db)), ChangeNotifierProvider.value( value: PlaylistService(db)), ChangeNotifierProvider.value(value: einstellungen), // Ohne diesen Provider stürbe schon das erste Antippen der neuen // Aktion in ladeAufServer mit ProviderNotFoundException. ChangeNotifierProvider.value(value: sync), ], child: MaterialApp( home: Scaffold( body: SortableSongList( songs: lieder, storeKey: SortStore.meineMusik, serverUpload: serverUpload, ), ), ), ), ); await beruhige(tester); await tester.longPress(find.text('Titel 0')); await beruhige(tester); return sync; } testWidgets('in „Meine Musik" erscheint die Server-Aktion', (tester) async { await pumpe(tester, serverUpload: true); expect(find.byTooltip('Auf den Server laden'), findsOneWidget); await aufraeumen(tester); }); testWidgets('in den übrigen Ansichten erscheint sie nicht', (tester) async { await pumpe(tester, serverUpload: false); // Der Auswahl-Modus läuft, die beiden Bestands-Aktionen sind da … expect(find.byTooltip('Zur Warteschlange hinzufügen'), findsOneWidget); // … die neue nicht. expect(find.byTooltip('Auf den Server laden'), findsNothing); await aufraeumen(tester); }); testWidgets('das Antippen reicht die Auswahl an den Upload weiter', (tester) async { final sync = await pumpe(tester, serverUpload: true); await tester.tap(find.byTooltip('Auf den Server laden')); await beruhige(tester); // Ein sichtbarer Knopf ist noch keine Funktion: geprüft wird, dass genau // der lang gedrückte Titel bei ladeAusgewaehlteHoch ankommt. expect(sync.hochgeladen, ['song-0']); expect(find.text('1 hochgeladen'), findsOneWidget); await aufraeumen(tester); }); }