Author SHA1 Message Date
Hermes (Server)andClaude Opus 5 a44321c510 Hook: GIT_DIR laesst Flutter das falsche Repo als SDK lesen
Git reicht GIT_DIR, GIT_INDEX_FILE und GIT_WORK_TREE an Hooks weiter. Der
Hook ruft flutter auf, und Flutter ermittelt seine eigene Version per git in
seinem SDK-Verzeichnis - mit geerbtem GIT_DIR liest es stattdessen DIESES
Repo und erfindet daraus eine SDK-Version.

Nachgemessen (flutter --version, gleiche Maschine, nur GIT_DIR variiert):
  GIT_DIR=<repo>/.git/worktrees/<name>  ->  3.48.0-0.2.pre, channel
                                            [user-branch], revision =
                                            Commit dieses Branches
  GIT_DIR=<repo>/.git                   ->  3.48.0-0.2.pre, channel main,
                                            revision = Commit von main
  ohne GIT_DIR                          ->  3.44.8, channel stable

Es betrifft also BEIDE Faelle, nicht nur verlinkte Worktrees - nur das
sichtbare Symptom unterscheidet sich:
- Aus einem verlinkten Worktree scheitert die Abhaengigkeitsaufloesung
  ("The current Flutter SDK version is 0.0.0-unknown") und der Commit wird
  abgelehnt, obwohl mit dem Code alles in Ordnung ist.
- Aus dem Haupt-Checkout laeuft das Gate zwar durch, aber Flutter baut sein
  Werkzeug bei jedem Commit neu ("Building flutter tool...", ein bis zwei
  Minuten). Diesen Preis zahlt dort bisher jeder Commit unbemerkt.

Zusaetzlich gibt Flutter dabei die Remote-URL des gelesenen Repos aus. Da
der Zugriffstoken in dieser URL eingebettet ist, stand er so in jeder
Hook-Ausgabe. Der Fix schliesst auch diesen Weg. (Die Token-Ablage selbst
gehoert getrennt behandelt - Credential-Helper statt URL-Einbettung.)

Fix: Repo-Wurzel merken, dann die von git gesetzten Variablen loeschen,
bevor flutter laeuft.

Getestet: Hook mit gesetztem GIT_DIR und GIT_INDEX_FILE aus einem
verlinkten Worktree aufgerufen - analyze und test laufen beide durch,
Exit 0. Unabhaengig von der zweiten Session im Haupt-Checkout bestaetigt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbuWT267W8rFDsaavANuEn
2026-08-21 15:33:38 +02:00
Hermes (Server)andClaude Opus 5 34d6d8e52a Zweiter Tab heisst wieder "Download" statt "Online"
Auf Wunsch von Dustin. Der Inhalt bleibt unveraendert (YouTube-Downloader
und Server-Bibliothek als Unterreiter), nur die Beschriftung aendert sich.
Die vier Bereiche: Meine Musik, Download, Suchen, Favoriten.

- Symbol ebenfalls zurueck auf Icons.download_outlined/download. Das ist
  kein neuer Entwurf: genau dieses Symbol trug der Tab, bevor er in
  Commit a625d70 in "Online" umbenannt wurde.
- Die Ueberschrift im Bereich selbst stand noch auf "Online" — ein Tab
  "Download", der auf einen Bildschirm "Online" fuehrt, waere verwirrend.
  Zieht jetzt nach.
- Tests und Testbeschreibungen entsprechend angepasst.

328 Tests gruen (1 uebersprungen), flutter analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
2026-08-21 14:22:36 +02:00
Hermes (Server)andClaude Opus 5 5b6fa949a9 Hauptmenue wieder deutlich sichtbar
Entwarnung vorweg: es fehlt nichts. Alle vier Bereiche (Meine Musik,
Online, Suchen, Favoriten) standen durchgehend in der Bottom-Nav;
lib/main.dart wurde von der UI-Politur gar nicht angefasst.

Aber die Politur hatte das Menue abgeschwaecht — zwei eigene Fehler:
- selectedLabelStyle/unselectedLabelStyle bekamen beide fontSize 12. Damit
  verlor der aktive Eintrag den Groessenunterschied (Flutter-Vorgabe 14 zu
  12), an dem man erkennt, wo man ist. Jetzt keine fontSize mehr.
- unselectedItemColor stand auf text3 (Beiwerk-Stufe). Ein Hauptmenue ist
  kein Beiwerk -> text2.

Zusaetzlich deutlicher: Haarlinie ueber der Leiste (auf Schwarz ging sie
sonst im Inhalt auf), gefuellte Symbole fuer den aktiven Bereich (zweites
Merkmal neben der Farbe) und fetterer aktiver Eintrag.

Neu: test/hauptmenue_test.dart — prueft Vorhandensein, Sichtbarkeit auf dem
Bildschirm, Position am unteren Rand, Wechsel per Tipp und die Abhebung des
aktiven Bereichs. Damit kann keine weitere Politur das Menue unbemerkt
abschwaechen.

Nebenbei eine Test-Falle geloest: await db.close() nach einem tester.tap
laesst den Lauf unbegrenzt haengen (drift plant eine Aufraeum-Aufgabe ein,
die der Test-Rahmen nicht mehr abarbeitet). Rezept im Test dokumentiert.
Beim bekannten Haenger in song_detail_sheet_test.dart hilft es NICHT —
gegengeprueft und als eigene Ursache im BACKLOG vermerkt.

328 Tests gruen (10 neue, 1 uebersprungen), flutter analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
2026-08-21 14:13:11 +02:00
Hermes (Server)andClaude Opus 5 1547f43099 Merge: UI-Politur (Kontrast, Abstaende, Typografie, Bewegung)
Kein Redesign — Schwarz/Rot, 4 Tabs und Lieder/Kategorie unveraendert.

- Textkontrast: white38 (3,4:1) verfehlte die WCAG-Grenze von 4,5:1 an 17
  Stellen. Drei benannte Stufen ersetzen ihn; ein Test rechnet die
  Verhaeltnisse bei jedem Lauf nach.
- Reiter: 48 dp statt ~38 (Material 3), mit Rueckmeldung beim Antippen und
  weichem Uebergang; Auswahl auch fuer Vorlesehilfen erkennbar.
- Abstaende auf 8er-Raster, Schriftgroessen als 7-stufige Skala.
- Mini-Player ueberarbeitet, Layout-Widerspruch (Expanded um feste Hoehe)
  entschaerft.
- Emoji aus Bedienelementen entfernt.

318 Tests gruen (1 uebersprungen, siehe BACKLOG.md), analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
2026-08-21 12:59:27 +02:00
9 changed files with 320 additions and 13 deletions
+17 -1
View File
@@ -8,9 +8,25 @@ export PATH="$HOME/development/flutter/bin:$PATH"
echo "🔍 Melo App Qualitäts-Gate gestartet..." echo "🔍 Melo App Qualitäts-Gate gestartet..."
# Git reicht GIT_DIR/GIT_INDEX_FILE/GIT_WORK_TREE an Hooks weiter. Flutter
# ermittelt seine Version per git im SDK-Verzeichnis — erbt es dabei GIT_DIR,
# liest es stattdessen DIESES Repo und erfindet daraus eine SDK-Version.
# Betrifft BEIDE Arbeitsbäume, nur unterschiedlich sichtbar:
# verlinkter Worktree -> harter Abbruch ("SDK version is 0.0.0-unknown"),
# korrekte Commits werden abgelehnt
# Haupt-Checkout -> still: Flutter baut sein Werkzeug bei JEDEM Commit
# neu (ein bis zwei Minuten pro Commit)
# Gemessen: ohne GIT_DIR meldet Flutter 3.44.8/stable, mit GIT_DIR dagegen
# 3.48.0-0.2.pre und als Revision einen Commit AUS DIESEM REPO.
# Nebeneffekt: Flutter gibt dabei die Remote-URL aus — steht dort ein Token
# in der URL, landet es in der Hook-Ausgabe. Das unset schließt auch das.
ROOT="$(git rev-parse --show-toplevel)" || exit 1
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_PREFIX GIT_COMMON_DIR
cd "$ROOT" || exit 1
# 1. Dart Analyse # 1. Dart Analyse
echo -n " Prüfe: flutter analyze ... " echo -n " Prüfe: flutter analyze ... "
cd "$(git rev-parse --show-toplevel)" || exit 1
ANALYZE_OUTPUT=$($FLUTTER analyze 2>&1) ANALYZE_OUTPUT=$($FLUTTER analyze 2>&1)
if echo "$ANALYZE_OUTPUT" | grep -q "No issues found"; then if echo "$ANALYZE_OUTPUT" | grep -q "No issues found"; then
echo "✅" echo "✅"
+11
View File
@@ -28,8 +28,19 @@ den nächsten Bibliotheks-Scan damit überlebt.
| `http.runWithClient` erreicht den State im Modal-Sheet nicht | ❌ widerlegt — eigene Sonde zeigt `MockClient` auch im Sheet | | `http.runWithClient` erreicht den State im Modal-Sheet nicht | ❌ widerlegt — eigene Sonde zeigt `MockClient` auch im Sheet |
| drift unter `testWidgets` blockiert (fehlendes `runAsync`) | ❌ widerlegt — `upsertSongs` + `watchSongs().first` laufen durch | | drift unter `testWidgets` blockiert (fehlendes `runAsync`) | ❌ widerlegt — `upsertSongs` + `watchSongs().first` laufen durch |
| Einer der Provider blockiert `pumpWidget` | ❌ widerlegt — `MeloDb`, `CategoryService`, `AppSettings` einzeln geprüft | | Einer der Provider blockiert `pumpWidget` | ❌ widerlegt — `MeloDb`, `CategoryService`, `AppSettings` einzeln geprüft |
| `db.close()` im Teardown (der Hänger aus `hauptmenue_test.dart`) | ❌ widerlegt — Entfernen ändert nichts, siehe unten |
| Zusammenspiel `showModalBottomSheet` + Providerbaum | ⏳ offen — der Hänger tritt zwischen `pumpWidget` und dem ersten `tap` auf | | Zusammenspiel `showModalBottomSheet` + Providerbaum | ⏳ offen — der Hänger tritt zwischen `pumpWidget` und dem ersten `tap` auf |
**Nicht die Ursache (2026-08-21 geprüft):** In `hauptmenue_test.dart` trat ein
Hänger mit derselben Signatur auf. Dort war die Ursache `await db.close()`
nach einem `tester.tap` — drift plant beim Schließen eine Aufräum-Aufgabe
ein, die der Test-Rahmen nicht mehr abarbeitet. Das Rezept dagegen:
Dienste per `.value` bereitstellen (der Baum darf sie nicht schließen), am
Testende `pumpWidget(SizedBox())` + `pump(Duration(milliseconds: 1))`, und
auf `db.close()` verzichten. **Bei `song_detail_sheet_test.dart` hilft genau
das nicht** — dort wurde es angewandt und der Test hängt weiter. Die beiden
Hänger haben also unterschiedliche Ursachen.
**Nächster Schritt:** Den Testkörper schrittweise abspecken (Providerbaum **Nächster Schritt:** Den Testkörper schrittweise abspecken (Providerbaum
minimieren, Sheet durch eine normale Route ersetzen) bis der Hänger minimieren, Sheet durch eine normale Route ersetzen) bis der Hänger
verschwindet — die letzte entfernte Zutat ist die Ursache. Ein verschwindet — die letzte entfernte Zutat ist die Ursache. Ein
+53
View File
@@ -5,6 +5,59 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
## [Unreleased] ## [Unreleased]
### 🏷️ Der zweite Tab heißt wieder „Download" (2026-08-21)
- 📥 **Aus „Online" wird „Download".** Der Bereich behält seinen Inhalt
unverändert — YouTube-Downloader und Server-Bibliothek als Unterreiter.
Nur die Beschriftung im Hauptmenü ändert sich.
Die vier Bereiche heißen jetzt: **Meine Musik · Download · Suchen ·
Favoriten**.
- ⬇️ **Passendes Symbol dazu.** Statt der Note steht wieder ein Download-Pfeil
im Menü — genau das Symbol, das der Tab schon einmal hatte, bevor er in
„Online" umbenannt wurde. Also nichts Neues erfunden, sondern der frühere
Stand wiederhergestellt.
- 📄 **Auch die Überschrift im Bereich selbst zieht nach.** Sie stand noch auf
„Online" — ein Tab namens „Download", der auf einen Bildschirm namens
„Online" führt, wäre verwirrend gewesen.
-**Getestet:** 328 Tests grün, `flutter analyze` ohne Befund.
### 🧭 Hauptmenü wieder deutlich sichtbar (2026-08-21)
**Zuerst die Entwarnung:** Es fehlt nichts und nichts ist verschoben. Alle
vier Bereiche standen und stehen unten im Hauptmenü — `lib/main.dart` wurde
von der UI-Politur überhaupt nicht angefasst.
- 🐛 **Behoben: die Politur hatte das Menü abgeschwächt.** Zwei Fehler von
mir aus der letzten Runde:
- Ich hatte für **aktiv und inaktiv dieselbe Schriftgröße** (12) gesetzt.
Damit verlor der aktive Bereich den Größenunterschied (sonst 14 zu 12),
an dem man auf einen Blick erkennt, wo man ist.
- Die inaktiven Einträge liefen auf der **blassen Beiwerk-Stufe**. Ein
Hauptmenü ist kein Beiwerk — sie sind jetzt wieder kräftig.
-**Zusätzlich deutlicher gemacht:**
- Eine **Haarlinie über dem Menü** — auf schwarzem Grund ging die Leiste
sonst optisch im Inhalt auf.
- Der aktive Bereich hat jetzt ein **gefülltes Symbol** (ausgefülltes Herz,
volle Note). Wer Rot schlecht sieht, erkennt trotzdem, wo er steht —
vorher unterschied nur die Farbe.
- Der aktive Eintrag ist fetter gesetzt.
- 🔒 **Damit das keine Politur mehr abschwächen kann:** ein neuer Test prüft,
dass alle vier Bereiche vorhanden **und auf dem Bildschirm** sind, dass das
Menü am unteren Rand klebt, dass jeder Tipp dorthin wechselt, und dass der
aktive Bereich sich nicht nur durch Farbe abhebt.
- 🔧 **Details für Technikinteressierte:**
- Neu: `test/hauptmenue_test.dart` (10 Prüfungen).
- `bottomNavigationBarTheme`: keine feste `fontSize` mehr (damit Flutters
14/12 gelten), `unselectedItemColor` von `text3` auf `text2`,
`showUnselectedLabels` ausdrücklich gesetzt.
- Beim Schreiben des Tests ist eine **Test-Falle** aufgefallen und gelöst:
`await db.close()` nach einem `tester.tap` lässt den Testlauf
**unbegrenzt hängen**. Rezept dagegen steht im Test und in `BACKLOG.md`.
Beim schon bekannten Hänger in `song_detail_sheet_test.dart` hilft es
allerdings nicht — dort ist die Ursache eine andere, jetzt ebenfalls im
Backlog vermerkt.
-**Getestet:** 328 Tests grün (10 neue), `flutter analyze` ohne Befund.
### ✨ UI-Politur: Kontrast, Abstände, Typografie, Bewegung (2026-08-21) ### ✨ UI-Politur: Kontrast, Abstände, Typografie, Bewegung (2026-08-21)
Kein neues Design — dasselbe Melo in sauber. Schwarz/Rot, die vier Tabs und Kein neues Design — dasselbe Melo in sauber. Schwarz/Rot, die vier Tabs und
+3 -2
View File
@@ -18,7 +18,8 @@ import '../shared/sub_tabs.dart';
import 'download_einordnung.dart'; import 'download_einordnung.dart';
import '../shared/theme.dart'; import '../shared/theme.dart';
/// Online-Tab: durchsucht den verbundenen Navidrome-Server. Abgespielte /// Download-Tab: YouTube-Downloader und der verbundene Navidrome-Server
/// (Unterreiter). Abgespielte
/// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert /// Titel werden vom [MeloAudioHandler] automatisch lokal zwischengespeichert
/// und stehen danach offline zur Verfügung. /// und stehen danach offline zur Verfügung.
class DownloadsScreen extends StatefulWidget { class DownloadsScreen extends StatefulWidget {
@@ -42,7 +43,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
padding: EdgeInsets.fromLTRB(20, 16, 20, 4), padding: EdgeInsets.fromLTRB(20, 16, 20, 4),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text('Online', child: Text('Download',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)), style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
), ),
), ),
+14 -3
View File
@@ -176,19 +176,30 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
const MiniPlayer(), const MiniPlayer(),
], ],
), ),
bottomNavigationBar: BottomNavigationBar( // Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
bottomNavigationBar: DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.border)),
),
child: BottomNavigationBar(
currentIndex: _index, currentIndex: _index,
onTap: _goTo, onTap: _goTo,
items: const [ items: const [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.headphones), label: 'Meine Musik'), icon: Icon(Icons.headphones), label: 'Meine Musik'),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.music_note_outlined), label: 'Online'), icon: Icon(Icons.download_outlined),
activeIcon: Icon(Icons.download),
label: 'Download'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'), BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.favorite_border), label: 'Favoriten'), icon: Icon(Icons.favorite_border),
activeIcon: Icon(Icons.favorite),
label: 'Favoriten'),
], ],
), ),
),
); );
} }
} }
+11 -4
View File
@@ -167,14 +167,21 @@ class MeloTheme {
BorderRadius.vertical(top: Radius.circular(MeloRadius.sheet)), BorderRadius.vertical(top: Radius.circular(MeloRadius.sheet)),
), ),
), ),
// Das Hauptmenü. Es muss auf den ersten Blick als solches erkennbar
// sein — deshalb bewusst kräftiger als der übrige Beiwerk-Text:
// - text2 (72 %) statt der blassen dritten Stufe für die inaktiven
// Einträge; sie sollen lesbar sein, nicht nur erahnbar.
// - KEINE fontSize hier: damit gelten Flutters Vorgaben von 14 (aktiv)
// und 12 (inaktiv). Eine feste Größe für beide nahm dem aktiven
// Reiter genau den Größenunterschied, an dem man ihn erkennt.
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: surface, backgroundColor: surface,
selectedItemColor: red, selectedItemColor: red,
unselectedItemColor: text3, unselectedItemColor: text2,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
selectedLabelStyle: showUnselectedLabels: true,
TextStyle(fontSize: 12, fontWeight: FontWeight.w600), selectedLabelStyle: TextStyle(fontWeight: FontWeight.w700),
unselectedLabelStyle: TextStyle(fontSize: 12), unselectedLabelStyle: TextStyle(fontWeight: FontWeight.w500),
), ),
); );
} }
+1 -1
View File
@@ -87,7 +87,7 @@ void main() {
await db.close(); await db.close();
}); });
testWidgets('Online-Tab hat die Unterreiter YouTube und Server', testWidgets('Download-Tab hat die Unterreiter YouTube und Server',
(tester) async { (tester) async {
final auth = final auth =
BakaAuth(client: MockClient((_) async => http.Response('', 500)), BakaAuth(client: MockClient((_) async => http.Response('', 500)),
+208
View File
@@ -0,0 +1,208 @@
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:melo/library/category_service.dart';
import 'package:melo/library/database.dart';
import 'package:melo/library/library_service.dart';
import 'package:melo/library/playlist_service.dart';
import 'package:melo/main.dart';
import 'package:melo/player/audio_handler.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/melo_cloud_service.dart';
import 'package:melo/services/offline_mode.dart';
import 'package:melo/services/sync_service.dart';
import 'package:melo/services/yt_download_service.dart';
import 'package:melo/settings/app_settings.dart';
import 'package:melo/shared/theme.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _MemorySpeicher implements TokenSpeicher {
final Map<String, String> werte = {};
@override
Future<String?> lesen(String key) async => werte[key];
@override
Future<void> schreiben(String key, String wert) async => werte[key] = wert;
@override
Future<void> loeschen(String key) async => werte.remove(key);
}
/// Die vier Bereiche des Hauptmenüs, in dieser Reihenfolge.
const hauptmenue = ['Meine Musik', 'Download', 'Suchen', 'Favoriten'];
void main() {
late MeloDb db;
late CategoryService kategorien;
late LibraryService bibliothek;
late PlaylistService listen;
late BakaAuth auth;
late AppSettings einstellungen;
late OfflineMode offline;
late YtDownloadService ytDownload;
late SyncService sync;
MeloAudioHandler? handler;
setUp(() {
SharedPreferences.setMockInitialValues({});
db = MeloDb(NativeDatabase.memory());
kategorien = CategoryService(db);
bibliothek = LibraryService(db);
listen = PlaylistService(db);
auth = BakaAuth(speicher: _MemorySpeicher());
einstellungen = AppSettings();
offline = OfflineMode();
ytDownload = YtDownloadService(auth: auth);
sync = SyncService(db: db, cloud: MeloCloudService(auth: auth));
});
tearDown(() {
kategorien.dispose();
handler?.dispose();
// Bewusst kein db.close(): drift plant beim Schließen eine Aufräum-
// Aufgabe ein, die im Test-Rahmen nach einem tester.tap nie mehr an die
// Reihe kommt — der Lauf hinge dann unbegrenzt. Die Datenbank liegt nur
// im Arbeitsspeicher und verschwindet mit dem Testprozess.
});
// Alle Dienste gehören dem Test und werden per .value gereicht. Würde der
// Widget-Baum sie beim Abbauen selbst schließen, bliebe ein Aufräum-Timer
// von drift im Test-Rahmen hängen — und ein anschließendes db.close()
// käme gar nicht mehr zurück (der Testlauf hinge unbegrenzt).
Widget bauen() {
// Der Audio-Handler erst hier: sein Zwischenspeicher greift auf einen
// Plattform-Kanal zu, den es im setUp noch nicht gibt.
handler ??= MeloAudioHandler(db: db);
return MultiProvider(
providers: [
Provider<MeloDb>.value(value: db),
ChangeNotifierProvider<LibraryService>.value(value: bibliothek),
ChangeNotifierProvider<PlaylistService>.value(value: listen),
Provider<MeloAudioHandler>.value(value: handler!),
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
ChangeNotifierProvider<OfflineMode>.value(value: offline),
ChangeNotifierProvider<BakaAuth>.value(value: auth),
ChangeNotifierProvider<YtDownloadService>.value(value: ytDownload),
ChangeNotifierProvider<SyncService>.value(value: sync),
],
child: MaterialApp(theme: MeloTheme.dark, home: const HomeShell()),
);
}
/// Baut den Baum ab und beendet die laufenden Timer.
///
/// Muss im Testkörper passieren, nicht im tearDown: der Positions-Timer
/// des Audio-Handlers läuft periodisch, und der Test-Rahmen prüft am Ende
/// des Körpers, ob noch Timer offen sind.
Future<void> abbauen(WidgetTester tester) async {
await tester.pumpWidget(const SizedBox());
handler?.dispose();
handler = null;
// Die Zeit ein Stück vorstellen: beim Abbauen melden sich die
// Datenbank-Ströme ab und planen dafür eine Aufräum-Aufgabe ein. Ein
// pump() ohne Dauer lässt sie liegen, und der Test-Rahmen meldet dann
// einen offenen Timer.
await tester.pump(const Duration(milliseconds: 1));
}
Finder eintrag(String name) => find.descendant(
of: find.byType(BottomNavigationBar),
matching: find.text(name),
);
testWidgets('alle vier Bereiche stehen im Hauptmenü', (tester) async {
await tester.pumpWidget(bauen());
await tester.pump();
final leiste =
tester.widget<BottomNavigationBar>(find.byType(BottomNavigationBar));
expect(leiste.items.map((i) => i.label).toList(), hauptmenue);
await abbauen(tester);
});
testWidgets('jeder Bereich ist beschriftet und liegt auf dem Bildschirm',
(tester) async {
await tester.pumpWidget(bauen());
await tester.pump();
final bildschirm = tester.view.physicalSize / tester.view.devicePixelRatio;
for (final name in hauptmenue) {
expect(eintrag(name), findsOneWidget, reason: '"$name" fehlt im Menü');
// Vorhanden genügt nicht — ein Eintrag außerhalb des Bildschirms oder
// ohne Ausdehnung wäre für den Nutzer nicht da.
final kasten = tester.getRect(eintrag(name));
expect(kasten.width, greaterThan(0), reason: '"$name" ohne Breite');
expect(kasten.height, greaterThan(0), reason: '"$name" ohne Höhe');
expect(kasten.bottom, lessThanOrEqualTo(bildschirm.height),
reason: '"$name" liegt unterhalb des Bildschirms');
expect(kasten.top, greaterThanOrEqualTo(0),
reason: '"$name" liegt oberhalb des Bildschirms');
}
await abbauen(tester);
});
for (var i = 1; i < hauptmenue.length; i++) {
final ziel = i;
testWidgets('Tipp auf "${hauptmenue[ziel]}" wechselt dorthin',
(tester) async {
await tester.pumpWidget(bauen());
await tester.pump();
await tester.tap(eintrag(hauptmenue[ziel]));
await tester.pump();
expect(
tester
.widget<BottomNavigationBar>(find.byType(BottomNavigationBar))
.currentIndex,
ziel,
);
await abbauen(tester);
});
}
testWidgets('das Hauptmenü klebt am unteren Rand', (tester) async {
// Läge es woanders, wäre es kein Hauptmenü mehr.
await tester.pumpWidget(bauen());
await tester.pump();
final menue = tester.getRect(find.byType(BottomNavigationBar));
final bildschirm = tester.view.physicalSize / tester.view.devicePixelRatio;
expect(menue.bottom, closeTo(bildschirm.height, 1));
await abbauen(tester);
});
group('Darstellung des Hauptmenüs', () {
// Reine Theme-Prüfungen — kein Widget-Baum nötig.
final menue = MeloTheme.dark.bottomNavigationBarTheme;
test('der aktive Bereich hebt sich ab — nicht nur durch Farbe', () {
// Wer Rot schlecht sieht, muss trotzdem erkennen, wo er ist.
expect(menue.selectedItemColor, MeloTheme.red);
expect(menue.selectedLabelStyle!.fontWeight, FontWeight.w700);
expect(menue.unselectedLabelStyle!.fontWeight, FontWeight.w500);
});
test('keine feste Schriftgröße — sonst verschwindet der Unterschied', () {
// Ohne Angabe gelten Flutters 14 (aktiv) und 12 (inaktiv). Beide auf
// 12 zu setzen nahm dem aktiven Reiter genau dieses Merkmal.
expect(menue.selectedLabelStyle!.fontSize, isNull);
expect(menue.unselectedLabelStyle!.fontSize, isNull);
});
test('die inaktiven Einträge sind lesbar, nicht nur erahnbar', () {
// text3 ist die Beiwerk-Stufe. Ein Hauptmenü ist kein Beiwerk.
expect(menue.unselectedItemColor, MeloTheme.text2);
});
test('die Beschriftungen bleiben auch bei inaktiven Einträgen stehen', () {
// Ohne sie müsste man die Symbole raten.
expect(menue.showUnselectedLabels, isTrue);
});
});
}
+2 -2
View File
@@ -30,7 +30,7 @@ void main() {
setUp(() => SharedPreferences.setMockInitialValues({})); setUp(() => SharedPreferences.setMockInitialValues({}));
testWidgets('Untere Navigation folgt der Referenz-UI: ' testWidgets('Untere Navigation folgt der Referenz-UI: '
'Meine Musik, Online, Suchen, Favoriten', (tester) async { 'Meine Musik, Download, Suchen, Favoriten', (tester) async {
final db = MeloDb(NativeDatabase.memory()); final db = MeloDb(NativeDatabase.memory());
final lib = LibraryService(db); final lib = LibraryService(db);
final playlists = PlaylistService(db); final playlists = PlaylistService(db);
@@ -60,7 +60,7 @@ void main() {
final bar = tester.widget<BottomNavigationBar>( final bar = tester.widget<BottomNavigationBar>(
find.byType(BottomNavigationBar)); find.byType(BottomNavigationBar));
expect(bar.items.map((i) => i.label).toList(), expect(bar.items.map((i) => i.label).toList(),
['Meine Musik', 'Online', 'Suchen', 'Favoriten']); ['Meine Musik', 'Download', 'Suchen', 'Favoriten']);
handler.dispose(); handler.dispose();
await db.close(); await db.close();