Tinker-Feedback: 5 Bugfixes (Shuffle/Repeat, Full-Cover, Auto-Play, Scan-Button, Back-Geste) #6
@@ -5,6 +5,60 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🎧 "Zuletzt" zeigte neu Hinzugefügtes statt zuletzt Gehörtem; Herz saß fern vom Songnamen (2026-08-29)
|
||||||
|
|
||||||
|
- 🐛 **Der Schnellzugriff "Zuletzt" auf "Meine Musik" zeigte "zuletzt hinzugefügte" Songs,
|
||||||
|
nicht "zuletzt gehörte"** — von Tinker gemeldet. Sowohl die Karte als auch der
|
||||||
|
Vollbild-Screen dahinter nutzten `db.watchRecent()`, das nach `dateAddedMs`
|
||||||
|
(Hinzufüge-Datum) sortiert, nicht nach dem tatsächlichen Abspielen.
|
||||||
|
- 🔧 **Fix:** neue Methode `MeloDb.watchRecentlyPlayed()` (`database.dart`) wertet
|
||||||
|
stattdessen die `PlaybackHistory`-Tabelle aus — eine Subquery ermittelt je Song per
|
||||||
|
`GROUP BY songId` / `MAX(playedAtMs)` nur den jüngsten Play-Zeitpunkt, damit ein
|
||||||
|
mehrfach gehörter Song nur einmal erscheint (an der Position seines letzten
|
||||||
|
Abspielens), zurückverknüpft auf `songs` (ohne getombstonte). "Zuletzt" in
|
||||||
|
`my_music_screen.dart` (Karte + Vollbild-Screen, `RecentlyAddedScreen` dafür in
|
||||||
|
`RecentlyPlayedScreen` umbenannt) nutzt jetzt diese Methode.
|
||||||
|
- 🐛 **Das Favoriten-Herz im Vollbild-Player saß oben in der AppBar**, weit weg vom
|
||||||
|
Songnamen — ebenfalls von Tinker gemeldet.
|
||||||
|
- 🔧 **Fix:** `_Herz` aus der AppBar-`actions`-Row entfernt und stattdessen direkt
|
||||||
|
neben den Titel in `_Angaben` gesetzt (`now_playing_screen.dart`).
|
||||||
|
- ✅ **Getestet:** 3 neue Unit-Tests für `watchRecentlyPlayed()` (RED→GREEN:
|
||||||
|
Mehrfach-Play erscheint nur einmal an der neuesten Position, mehrere Songs
|
||||||
|
absteigend sortiert, getombstonter Song mit Historie fehlt) sowie ein neuer
|
||||||
|
Widget-Test, der bestätigt, dass das Herz nicht mehr in der AppBar, sondern in
|
||||||
|
derselben Row wie der Songtitel sitzt. `flutter analyze` ohne Befund.
|
||||||
|
|
||||||
|
### 🔑 Auto-Anmeldung schlug beim App-Start fehl — betraf ALLE drei Nutzer (2026-08-29)
|
||||||
|
|
||||||
|
- 🐛 **Die automatische Baka-Anmeldung klappte beim App-Start oft nicht** —
|
||||||
|
die App fragte dann nach dem "Baka-Konto", obwohl die Zugangsdaten
|
||||||
|
korrekt waren. Betraf **alle drei Nutzer** (Dustin, Baka, Tinker), nicht
|
||||||
|
nur einen — von Dustin selbst gemeldet und bei allen drei bestätigt.
|
||||||
|
Grund: `main.dart` baut die 5 Haupt-Tabs über `IndexedStack` alle sofort
|
||||||
|
beim ersten Frame, nicht erst beim Umschalten. Der Download-Tab und die
|
||||||
|
YT-Suche lösen dabei beide unabhängig voneinander denselben Auto-Login
|
||||||
|
aus (`pruefeServerUser()` → `BakaAuth.autoAnmelden()`) — praktisch
|
||||||
|
gleichzeitig, mit denselben Zugangsdaten. `autoAnmelden()` hatte keine
|
||||||
|
Schutzvorkehrung gegen so einen doppelten Aufruf (der `istAngemeldet`-
|
||||||
|
Check lief synchron, bevor die erste Anfrage zurückkam), also gingen
|
||||||
|
zwei fast-gleichzeitige Login-Anfragen an denselben Server raus — der
|
||||||
|
lehnte vermutlich eine davon ab (Session-/Lock-Konflikt).
|
||||||
|
- 🔧 **Fix:** `BakaAuth.autoAnmelden()` in `baka_auth.dart` hängt einen
|
||||||
|
überlappenden Aufruf jetzt an den bereits laufenden Login-Versuch an
|
||||||
|
(`_laufenderAutoLogin`), statt einen zweiten Request loszuschicken.
|
||||||
|
- 🔧 **Nebenbei zwei kleine Anmelde-Dialoge korrigiert:** Der Anmelde-
|
||||||
|
Dialog im Download-Tab (`downloads_screen.dart`) zeigte das
|
||||||
|
Benutzername-Feld immer mit "Baka" vorausgefüllt, egal wer die App
|
||||||
|
nutzt — zeigt jetzt den tatsächlich bekannten Navidrome-Benutzernamen
|
||||||
|
(leer, falls keiner bekannt). Der Navidrome-Dialog in den Einstellungen
|
||||||
|
(`settings_screen.dart`) wählte unter Baka/Tinker/Dustin immer "Baka"
|
||||||
|
vor — wählt jetzt den zuletzt bekannten Benutzer vor, falls bekannt.
|
||||||
|
- ✅ **Getestet:** neuer Unit-Test für `autoAnmelden()` (RED→GREEN, zwei
|
||||||
|
gleichzeitige Aufrufe lösen nur noch einen HTTP-Request aus statt
|
||||||
|
zwei) sowie ein neuer Widget-Test für die Benutzername-Vorbelegung im
|
||||||
|
Download-Tab-Dialog (RED→GREEN). Volle Suite grün, `flutter analyze`
|
||||||
|
ohne Befund.
|
||||||
|
|
||||||
### ⬅️ Zurück-Geste auf Nicht-Start-Tabs minimierte die App (2026-08-27)
|
### ⬅️ Zurück-Geste auf Nicht-Start-Tabs minimierte die App (2026-08-27)
|
||||||
|
|
||||||
- 🐛 **Auf "Favoriten" (oder einem anderen Tab außer "Meine Musik") tippen und
|
- 🐛 **Auf "Favoriten" (oder einem anderen Tab außer "Meine Musik") tippen und
|
||||||
|
|||||||
@@ -212,6 +212,35 @@ class MeloDb extends _$MeloDb {
|
|||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Überwacht die zuletzt GEHÖRTEN Songs (nicht getombstonte), aus
|
||||||
|
/// [PlaybackHistory]. Ein mehrfach gespielter Song erscheint nur einmal —
|
||||||
|
/// an der Position seines jüngsten Abspielens. Dafür wird pro [songId]
|
||||||
|
/// zuerst der jüngste `playedAtMs`-Eintrag ermittelt (Subquery mit
|
||||||
|
/// `GROUP BY`), und darüber zurück auf [songs] verknüpft.
|
||||||
|
Stream<List<Song>> watchRecentlyPlayed({int limit = 50}) {
|
||||||
|
final letzterPlay = playbackHistory.playedAtMs.max();
|
||||||
|
final letztePlaysProSong = selectOnly(playbackHistory)
|
||||||
|
..addColumns([playbackHistory.songId, letzterPlay])
|
||||||
|
..groupBy([playbackHistory.songId]);
|
||||||
|
final subquery = Subquery(letztePlaysProSong, 'letzte_plays');
|
||||||
|
final letzterPlayRef = subquery.ref(letzterPlay);
|
||||||
|
|
||||||
|
final query = select(songs).join([
|
||||||
|
innerJoin(
|
||||||
|
subquery,
|
||||||
|
subquery.ref(playbackHistory.songId).equalsExp(songs.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(songs.deleted.equals(false))
|
||||||
|
..orderBy([
|
||||||
|
OrderingTerm(expression: letzterPlayRef, mode: OrderingMode.desc)
|
||||||
|
])
|
||||||
|
..limit(limit);
|
||||||
|
|
||||||
|
return query.watch().map((rows) => rows.map((r) => r.readTable(songs)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Alle Songs inkl. getombstonte. Intern für ID-Stabilität beim Scan.
|
/// Alle Songs inkl. getombstonte. Intern für ID-Stabilität beim Scan.
|
||||||
Future<List<Song>> allSongs() => select(songs).get();
|
Future<List<Song>> allSongs() => select(songs).get();
|
||||||
|
|||||||
@@ -234,17 +234,17 @@ class _QuickAccessRow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (sektionen.contains(AppSettings.sektionZuletzt))
|
if (sektionen.contains(AppSettings.sektionZuletzt))
|
||||||
StreamBuilder<List<Song>>(
|
StreamBuilder<List<Song>>(
|
||||||
stream: db.watchRecent(limit: 50),
|
stream: db.watchRecentlyPlayed(limit: 50),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final recent = snapshot.data ?? const <Song>[];
|
final recent = snapshot.data ?? const <Song>[];
|
||||||
return _QuickCard(
|
return _QuickCard(
|
||||||
icon: Icons.schedule,
|
icon: Icons.schedule,
|
||||||
label: 'Zuletzt',
|
label: 'Zuletzt',
|
||||||
subtitle: 'Neu hinzugefügt',
|
subtitle: 'Zuletzt gehört',
|
||||||
coverPath: recent.isEmpty ? null : recent.first.coverPath,
|
coverPath: recent.isEmpty ? null : recent.first.coverPath,
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => const RecentlyAddedScreen()),
|
builder: (_) => const RecentlyPlayedScreen()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -363,22 +363,22 @@ class _Empty extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vollbild-Liste der zuletzt hinzugefügten Songs (Schnellzugriff "Zuletzt").
|
/// Vollbild-Liste der zuletzt gehörten Songs (Schnellzugriff "Zuletzt").
|
||||||
class RecentlyAddedScreen extends StatelessWidget {
|
class RecentlyPlayedScreen extends StatelessWidget {
|
||||||
const RecentlyAddedScreen({super.key});
|
const RecentlyPlayedScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final db = context.read<MeloDb>();
|
final db = context.read<MeloDb>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Zuletzt hinzugefügt')),
|
appBar: AppBar(title: const Text('Zuletzt gehört')),
|
||||||
body: StreamBuilder<List<Song>>(
|
body: StreamBuilder<List<Song>>(
|
||||||
stream: db.watchRecent(limit: 100),
|
stream: db.watchRecentlyPlayed(limit: 100),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final songs = snapshot.data ?? const <Song>[];
|
final songs = snapshot.data ?? const <Song>[];
|
||||||
if (songs.isEmpty) {
|
if (songs.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text('Noch nichts hinzugefügt',
|
child: Text('Noch nichts abgespielt',
|
||||||
style: TextStyle(color: MeloTheme.text2)),
|
style: TextStyle(color: MeloTheme.text2)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,10 +49,7 @@ class NowPlayingScreen extends StatelessWidget {
|
|||||||
// mit der fragte die App den Server nach einem Titel namens
|
// mit der fragte die App den Server nach einem Titel namens
|
||||||
// "https://…" und bekam nie einen Text.
|
// "https://…" und bekam nie einen Text.
|
||||||
final songId = songIdOf(item) ?? navidromeIdOf(item) ?? '';
|
final songId = songIdOf(item) ?? navidromeIdOf(item) ?? '';
|
||||||
return Row(
|
return IconButton(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: songId.isEmpty
|
tooltip: songId.isEmpty
|
||||||
? 'Kein Songtext für diesen Titel verfügbar'
|
? 'Kein Songtext für diesen Titel verfügbar'
|
||||||
: 'Songtext',
|
: 'Songtext',
|
||||||
@@ -60,12 +57,6 @@ class NowPlayingScreen extends StatelessWidget {
|
|||||||
onPressed: songId.isEmpty
|
onPressed: songId.isEmpty
|
||||||
? null
|
? null
|
||||||
: () => _showLyrics(context, songId),
|
: () => _showLyrics(context, songId),
|
||||||
),
|
|
||||||
// Ein Titel der Bibliothek wird lokal favorisiert, einer
|
|
||||||
// vom Server am Server — beide bekommen dasselbe Herz,
|
|
||||||
// statt dass eines davon ausgegraut bleibt.
|
|
||||||
_Herz(item: item),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -240,13 +231,24 @@ class _Angaben extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
item.title,
|
item.title,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
// Ein Titel der Bibliothek wird lokal favorisiert, einer
|
||||||
|
// vom Server am Server — beide bekommen dasselbe Herz,
|
||||||
|
// statt dass eines davon ausgegraut bleibt.
|
||||||
|
_Herz(item: item),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: MeloSpace.sm),
|
const SizedBox(height: MeloSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
item.artist ?? 'Unbekannt',
|
item.artist ?? 'Unbekannt',
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:melo/library/database.dart';
|
||||||
|
|
||||||
|
Future<void> _insertSong(MeloDb db, String id, {bool deleted = false}) {
|
||||||
|
return db.into(db.songs).insert(SongsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
path: '/$id.mp3',
|
||||||
|
title: id,
|
||||||
|
dateAddedMs: 0,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
deleted: Value(deleted),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _play(MeloDb db, String songId, int playedAtMs) {
|
||||||
|
return db.into(db.playbackHistory).insert(PlaybackHistoryCompanion.insert(
|
||||||
|
songId: songId,
|
||||||
|
positionMs: 0,
|
||||||
|
playedAtMs: playedAtMs,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('ein mehrfach gespielter Song erscheint nur einmal, an der Position '
|
||||||
|
'seines jüngsten Abspielens', () async {
|
||||||
|
final db = MeloDb(NativeDatabase.memory());
|
||||||
|
await _insertSong(db, 'a');
|
||||||
|
await _insertSong(db, 'b');
|
||||||
|
await _play(db, 'a', 1000);
|
||||||
|
await _play(db, 'b', 2000);
|
||||||
|
await _play(db, 'a', 3000); // a erneut gespielt, jetzt jüngster Play
|
||||||
|
|
||||||
|
final result = await db.watchRecentlyPlayed().first;
|
||||||
|
|
||||||
|
expect(result.map((s) => s.id).toList(), ['a', 'b']);
|
||||||
|
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mehrere Songs erscheinen absteigend nach ihrem letzten Abspielen',
|
||||||
|
() async {
|
||||||
|
final db = MeloDb(NativeDatabase.memory());
|
||||||
|
await _insertSong(db, 'a');
|
||||||
|
await _insertSong(db, 'b');
|
||||||
|
await _insertSong(db, 'c');
|
||||||
|
await _play(db, 'a', 1000);
|
||||||
|
await _play(db, 'b', 3000);
|
||||||
|
await _play(db, 'c', 2000);
|
||||||
|
|
||||||
|
final result = await db.watchRecentlyPlayed().first;
|
||||||
|
|
||||||
|
expect(result.map((s) => s.id).toList(), ['b', 'c', 'a']);
|
||||||
|
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ein gelöschter Song mit Wiedergabe-Historie erscheint nicht',
|
||||||
|
() async {
|
||||||
|
final db = MeloDb(NativeDatabase.memory());
|
||||||
|
await _insertSong(db, 'a', deleted: true);
|
||||||
|
await _insertSong(db, 'b');
|
||||||
|
await _play(db, 'a', 2000);
|
||||||
|
await _play(db, 'b', 1000);
|
||||||
|
|
||||||
|
final result = await db.watchRecentlyPlayed().first;
|
||||||
|
|
||||||
|
expect(result.map((s) => s.id).toList(), ['b']);
|
||||||
|
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value, driftRuntimeOptions;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.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/library/song_list.dart';
|
||||||
|
import 'package:melo/player/audio_handler.dart';
|
||||||
|
import 'package:melo/player/now_playing_screen.dart';
|
||||||
|
import 'package:melo/settings/app_settings.dart';
|
||||||
|
import 'package:melo/shared/favorite_button.dart';
|
||||||
|
|
||||||
|
/// Ersetzt in [MeloAudioHandler.loadPlaylist] den echten just_audio-Zugriff —
|
||||||
|
/// dieselbe Klasse wie in song_list_navigation_test.dart, dort erprobt.
|
||||||
|
class _FakeAudioHandler extends MeloAudioHandler {
|
||||||
|
_FakeAudioHandler(MeloDb db) : super(db: db);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> loadPlaylist(List<MediaItem> items, {int startIndex = 0}) async {
|
||||||
|
queue.add(items);
|
||||||
|
if (items.isNotEmpty) mediaItem.add(items[startIndex]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MeloDb db;
|
||||||
|
late CategoryService kategorien;
|
||||||
|
late PlaylistService playlists;
|
||||||
|
late AppSettings einstellungen;
|
||||||
|
late _FakeAudioHandler handler;
|
||||||
|
|
||||||
|
void stelleKanaeleRuhig() {
|
||||||
|
final messenger =
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||||
|
messenger.setMockMethodCallHandler(
|
||||||
|
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||||
|
(call) async => '/tmp/melo-test',
|
||||||
|
);
|
||||||
|
messenger.setMockMethodCallHandler(
|
||||||
|
const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'),
|
||||||
|
(call) async => null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
stelleKanaeleRuhig();
|
||||||
|
db = MeloDb(NativeDatabase.memory());
|
||||||
|
kategorien = CategoryService(db);
|
||||||
|
playlists = PlaylistService(db);
|
||||||
|
einstellungen = AppSettings();
|
||||||
|
await einstellungen.init();
|
||||||
|
handler = _FakeAudioHandler(db);
|
||||||
|
await db.into(db.songs).insert(SongsCompanion.insert(
|
||||||
|
id: 'song-1',
|
||||||
|
path: '/music/roses.mp3',
|
||||||
|
title: 'Roses',
|
||||||
|
artist: const Value('Neoni'),
|
||||||
|
dateAddedMs: 0,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
handler.dispose();
|
||||||
|
kategorien.dispose();
|
||||||
|
playlists.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
Song derSong() => Song(
|
||||||
|
id: 'song-1',
|
||||||
|
path: '/music/roses.mp3',
|
||||||
|
title: 'Roses',
|
||||||
|
artist: 'Neoni',
|
||||||
|
dateAddedMs: 0,
|
||||||
|
updatedAtMs: 0,
|
||||||
|
deleted: false,
|
||||||
|
playCount: 0,
|
||||||
|
categoriesEdited: false,
|
||||||
|
metadataEdited: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Öffnet den Vollbild-Player über denselben Weg wie in
|
||||||
|
// song_list_navigation_test.dart (Tap auf eine SongZeile) — dieser Weg ist
|
||||||
|
// dort nachweislich unproblematisch, im Unterschied zu einem direkten
|
||||||
|
// Aufbau von NowPlayingScreen mit vorab befülltem MediaItem-Stream.
|
||||||
|
Future<void> oeffnePlayer(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider<MeloDb>.value(value: db),
|
||||||
|
Provider<MeloAudioHandler>.value(value: handler),
|
||||||
|
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
|
||||||
|
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||||
|
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: SongZeile(
|
||||||
|
song: derSong(),
|
||||||
|
warteschlange: [derSong()],
|
||||||
|
index: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Roses'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> raeumeAuf(WidgetTester tester) async {
|
||||||
|
// Ohne diesen Abschluss bleibt ein drift-Stream-Abo (Favoriten-Herz)
|
||||||
|
// beim Abbau des Baums als offener Fake-Timer stehen — dieselbe
|
||||||
|
// Aufräum-Zeile wie in song_list_navigation_test.dart.
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Herz sitzt neben dem Songnamen, nicht mehr in der AppBar',
|
||||||
|
(tester) async {
|
||||||
|
await oeffnePlayer(tester);
|
||||||
|
|
||||||
|
expect(find.byType(NowPlayingScreen), findsOneWidget);
|
||||||
|
|
||||||
|
// Nicht mehr in der AppBar:
|
||||||
|
final herzInAppBar = find.descendant(
|
||||||
|
of: find.byType(AppBar),
|
||||||
|
matching: find.byType(FavoriteButton),
|
||||||
|
);
|
||||||
|
expect(herzInAppBar, findsNothing);
|
||||||
|
|
||||||
|
// Aber sichtbar auf dem Bildschirm, in derselben Row wie der Titel.
|
||||||
|
final titelRow = find
|
||||||
|
.ancestor(of: find.text('Roses'), matching: find.byType(Row))
|
||||||
|
.first;
|
||||||
|
final herzBeimTitel = find.descendant(
|
||||||
|
of: titelRow,
|
||||||
|
matching: find.byType(FavoriteButton),
|
||||||
|
);
|
||||||
|
expect(herzBeimTitel, findsOneWidget);
|
||||||
|
|
||||||
|
await raeumeAuf(tester);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user