## Critical Blockers Fixed - Remove playbackState.addError() which broke audio_service permanently - Move error handling from audio_handler to song_list where UI context exists - Display error in SnackBar instead of destroying playback stream - Add escapeChar parameter to LIKE queries for correct wildcard handling - Fixes broken search for titles with _ or % characters - Add dismissScanError() method to allow closing error banner - Previously banner couldn't be dismissed (missing notifyListeners()) ## Improvements - Remove dead livePaths list, use companions.isNotEmpty instead - Add initial progress update to show total files when scan starts - Add tooltip to now_playing_screen Zurück button - Add proper error handling in song_list with async/await ## Testing - Add regression test for CRITICAL-1: empty scan doesn't delete songs - Add regression test for CRITICAL-2: tombstoned files restore on re-scan - All 11 tests pass Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
103 lines
3.6 KiB
Dart
103 lines
3.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:melo/library/database.dart';
|
|
import 'package:melo/library/scan_service.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
void main() {
|
|
final fixtures = p.join(Directory.current.path, 'test', 'fixtures', 'audio');
|
|
late MeloDb db;
|
|
late Directory coverDir;
|
|
|
|
setUp(() async {
|
|
db = MeloDb(NativeDatabase.memory());
|
|
coverDir = await Directory.systemTemp.createTemp('melo_covers');
|
|
});
|
|
tearDown(() async {
|
|
await db.close();
|
|
await coverDir.delete(recursive: true);
|
|
});
|
|
|
|
test('scanFolders liest Tags aus den Test-mp3s', () async {
|
|
final count = await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
expect(count, 3);
|
|
final songs = await db.watchSongs().first;
|
|
expect(
|
|
songs.map((s) => s.title).toSet(),
|
|
{'Nachtpuls', 'Taglicht', 'Herzschlag'},
|
|
);
|
|
final nacht = songs.firstWhere((s) => s.title == 'Nachtpuls');
|
|
expect(nacht.artist, 'Rotklang');
|
|
expect(nacht.album, 'Schwarz');
|
|
expect(nacht.durationMs, greaterThan(0));
|
|
});
|
|
|
|
test('extrahiert eingebettetes Cover als Datei', () async {
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
final songs = await db.watchSongs().first;
|
|
final withCover = songs.where((s) => s.coverPath != null).toList();
|
|
// Nur "Nachtpuls" hat ein eingebettetes Cover.
|
|
expect(withCover.length, 1);
|
|
expect(withCover.first.title, 'Nachtpuls');
|
|
expect(File(withCover.first.coverPath!).existsSync(), isTrue);
|
|
});
|
|
|
|
test('Re-Scan behält die UUID (sync-stabil)', () async {
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
final firstIds = {
|
|
for (final s in await db.watchSongs().first) s.path: s.id
|
|
};
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
final secondIds = {
|
|
for (final s in await db.watchSongs().first) s.path: s.id
|
|
};
|
|
expect(secondIds, firstIds);
|
|
expect((await db.watchSongs().first).length, 3); // keine Duplikate
|
|
});
|
|
|
|
test('CRITICAL-1: leerer Scan loescht nicht (nonexistent folder)', () async {
|
|
// Initialer Scan
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
final initialCount = (await db.watchSongs().first).length;
|
|
expect(initialCount, 3);
|
|
|
|
// Scan mit nicht-existierendem Ordner (livePaths wird leer)
|
|
await scanFolders(db, ['/this/does/not/exist'], coverDir: coverDir);
|
|
|
|
// Alle Songs sollten noch da sein
|
|
final afterEmpty = await db.watchSongs().first;
|
|
expect(afterEmpty.length, initialCount);
|
|
});
|
|
|
|
test('CRITICAL-2: Datei wird nach Tombstone wieder sichtbar', () async {
|
|
// Initialer Scan: alle 3 Dateien
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
var songs = await db.watchSongs().first;
|
|
expect(songs.length, 3);
|
|
final nachtpulsId = songs.firstWhere((s) => s.title == 'Nachtpuls').id;
|
|
|
|
// Manuell tombstonen
|
|
await db.upsertSongs([
|
|
SongsCompanion.insert(
|
|
id: nachtpulsId,
|
|
path: p.join(fixtures, '01 Nachtpuls.mp3'),
|
|
title: 'Nachtpuls',
|
|
dateAddedMs: 1,
|
|
updatedAtMs: 1,
|
|
deleted: const Value(true),
|
|
),
|
|
]);
|
|
songs = await db.watchSongs().first;
|
|
expect(songs.where((s) => s.title == 'Nachtpuls').length, 0); // nicht sichtbar
|
|
|
|
// Re-Scan sollte die Datei wieder finden und deleted=false setzen
|
|
await scanFolders(db, [fixtures], coverDir: coverDir);
|
|
songs = await db.watchSongs().first;
|
|
expect(songs.where((s) => s.title == 'Nachtpuls').length, 1); // wieder da
|
|
expect(songs.length, 3); // alle 3 Songs
|
|
});
|
|
}
|