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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0fe624d1e8
commit
bc58438262
@@ -0,0 +1,128 @@
|
||||
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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user