Files
Melo/test/player/now_playing_screen_overflow_test.dart
T
Hermes (Server)andClaude Sonnet 5 bc58438262 Fix: RenderFlex-Overflow in NowPlayingScreen während der Öffnen-Geste
Bug (Code-Review, verifiziert): _Angaben (Titel/Künstler/Fortschritt/
Controls) hat eine intrinsische Mindesthöhe, die während der Öffnen-Geste
(progress zwischen ~0.3 und ~1.0, siehe main.dart Positioned.fromRect mit
Rect.lerp Mini-/Vollbild-Player) das noch kleinere, interpolierte
Rechteck übersteigt — Flutter meldete dann "RenderFlex overflowed" bei
praktisch jeder normalen Öffnen-Geste.

Fix: der Inhalt-Slot (in beiden responsiven Zweigen — Row nebeneinander
und Column untereinander) steckt jetzt in
ClipRect(child: ConstrainedBox(maxHeight: raum.maxHeight - Lücke,
child: OverflowBox(maxHeight: infinity, fit: deferToChild, child: _Angaben))).
ConstrainedBox deckelt nur, was OverflowBox nach AUSSEN als eigene Größe
meldet (verhindert, dass der umgebende Row/Column selbst überläuft);
OverflowBox gibt _Angaben davon unabhängig weiterhin unbegrenzte Höhe
(verhindert, dass _Angaben intern überläuft) — überschüssiger Inhalt wird
vom ClipRect während der kurzen, ohnehin noch recht durchsichtigen
Übergangsphase einfach nicht gemalt statt eine Overflow-Warnung
auszulösen. Bei ausreichend Platz (Endzustand progress==1) ist
raum.maxHeight groß genug, dass nichts geklemmt wird — die Optik bleibt
dort unverändert (now_playing_heart_test.dart bestätigt das weiterhin).

Test zuerst (RED: "RenderFlex overflowed by 13 pixels" reproduziert, mit
NowPlayingScreen in einem echten, interpolierten Zwischenrechteck bei
progress≈0.4 auf Basis der HomeShell-Rect-Formel): test/player/
now_playing_screen_overflow_test.dart.

Volle Suite (626 Tests) grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SM6e73V9ZethPiqHSocxUA
2026-08-29 15:55:52 +02:00

129 lines
4.9 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:drift/drift.dart' show 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:melo/library/category_service.dart';
import 'package:melo/library/database.dart';
import 'package:melo/library/playlist_service.dart';
import 'package:melo/player/audio_handler.dart';
import 'package:melo/player/mini_player.dart';
import 'package:melo/player/now_playing_screen.dart';
import 'package:melo/player/player_expansion_controller.dart';
import 'package:melo/settings/app_settings.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Ersetzt in [MeloAudioHandler.loadPlaylist] den echten just_audio-Zugriff —
/// dieselbe Klasse wie in now_playing_heart_test.dart.
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() {
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(() => SharedPreferences.setMockInitialValues({}));
testWidgets(
'NowPlayingScreen in einem während der Öffnen-Geste noch kleinen, '
'interpolierten Rechteck (progress ~0.4) wirft keinen '
'RenderFlex-Overflow', (tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
stelleKanaeleRuhig();
final db = MeloDb(NativeDatabase.memory());
final kategorien = CategoryService(db);
final playlists = PlaylistService(db);
final einstellungen = AppSettings();
await einstellungen.init();
final handler = _FakeAudioHandler(db);
addTearDown(kategorien.dispose);
addTearDown(playlists.dispose);
// Ein echtes MediaItem, wie es beim Abspielen im mediaItem-Stream steht
// — NowPlayingScreen zeigt sonst nur "Nichts in Wiedergabe" und hat dann
// gar keinen Inhalt, der überlaufen könnte.
handler.mediaItem.add(const MediaItem(
id: 'song-1',
title: 'Roses',
artist: 'Neoni',
));
// Dieselbe Rect-Interpolation wie HomeShell._miniRect/_vollbildRect bei
// progress=0.4 auf der Standard-Testbildschirmgröße (800x600, siehe
// flutter_test-Default) — main.dart positioniert NowPlayingScreen
// während der Öffnen-Geste über genau so ein Positioned.fromRect mit
// Rect.lerp zwischen Mini- und Vollbild-Rechteck. Bei p>0.3 blendet der
// Inhalt laut Spec schon sichtbar ein (main.dart: Opacity((p-0.3)/0.7)),
// das Rechteck ist zu dem Zeitpunkt aber noch weit von der
// Vollbildgröße entfernt.
const p = 0.4;
const screenSize = Size(800, 600);
const miniHoehe = MiniPlayer.hoehe;
final unten = screenSize.height - miniHoehe - kBottomNavigationBarHeight;
final miniRect = Rect.fromLTWH(0, unten, screenSize.width, miniHoehe);
final vollbildRect =
Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
final rect = Rect.lerp(miniRect, vollbildRect, p)!;
final expansion = PlayerExpansionController(vsync: tester);
addTearDown(expansion.dispose);
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),
ChangeNotifierProvider<PlayerExpansionController>.value(
value: expansion),
],
child: MaterialApp(
home: Scaffold(
body: Stack(
children: [
Positioned.fromRect(
rect: rect,
child: const NowPlayingScreen(),
),
],
),
),
),
),
);
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
// Aufräum-Reihenfolge wie in den übrigen Tests dieser Datei-Familie:
// erst den Baum abbauen, dann den Handler disposen (offener Positions-
// Timer, siehe now_playing_heart_test.dart).
await tester.pumpWidget(const SizedBox());
handler.dispose();
await tester.pump(const Duration(milliseconds: 1));
});
}