Files
plezy/test/utils/music_navigation_test.dart
edde746 a1d8ab5210 fix(plex): surface instant mix failures instead of silently doing nothing
Tapping Instant Mix on Plex often did nothing: the /playQueues POST used no
endpoint failover, createPlayQueue swallowed every error into null,
fetchInstantMix mapped that to an empty list, playInstantMix returned
silently on empty tracks, and the errors stream's only listener is the
now-playing screen, which is not mounted at tap time (#2141).

The play-queue POST now rides the shared endpoint failover (replaying it is
safe: an orphaned duplicate queue on the server is inert), createPlayQueue
and fetchInstantMix propagate typed errors, playInstantMix reports
started/empty/superseded and throws fetch failures, and the tap site shows
a translated snackbar for a failed or empty mix. A failed collection or
playlist queue launch now also reaches the real failure snackbar instead of
the misleading "no items" path.
2026-08-27 16:59:08 +02:00

105 lines
3.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/services/music/music_playback_service.dart';
import 'package:plezy/services/playback_initialization_types.dart';
import 'package:plezy/utils/music_navigation.dart';
import 'package:provider/provider.dart';
import '../test_helpers/media_items.dart';
import '../test_helpers/stub_music_playback_service.dart';
/// Instant Mix must never be a silent no-op (#2141): the tap-site seam
/// ([playInstantMix]) owns the snackbar for a failed or empty mix, because
/// the now-playing screen — the `errors`-stream listener — is not mounted at
/// tap time. Mid-playback errors keep flowing through that stream instead.
class _OutcomeMusicService extends StubMusicPlaybackService {
_OutcomeMusicService({this.outcome, this.error});
final InstantMixOutcome? outcome;
final Object? error;
@override
Future<InstantMixOutcome> playInstantMix(MediaItem seed) async {
if (error != null) throw error!;
return outcome!;
}
}
void main() {
setUp(() => LocaleSettings.setLocaleSync(AppLocale.en));
Future<BuildContext> pumpWithService(WidgetTester tester, MusicPlaybackService service) async {
late BuildContext capturedContext;
await tester.pumpWidget(
ChangeNotifierProvider<MusicPlaybackService>.value(
value: service,
child: MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
capturedContext = context;
return const SizedBox.shrink();
},
),
),
),
),
);
return capturedContext;
}
final seed = testMediaItem(id: 'track-1', backend: MediaBackend.plex, kind: MediaKind.track);
testWidgets('a thrown fetch failure surfaces the generic instant mix snackbar', (tester) async {
final context = await pumpWithService(tester, _OutcomeMusicService(error: StateError('boom')));
await playInstantMix(context, seed);
await tester.pump();
expect(find.text(t.music.instantMixFailed), findsOneWidget);
});
testWidgets('a PlaybackException surfaces its own localized message', (tester) async {
final context = await pumpWithService(
tester,
_OutcomeMusicService(error: PlaybackException(t.music.instantMixNoServer)),
);
await playInstantMix(context, seed);
await tester.pump();
expect(find.text(t.music.instantMixNoServer), findsOneWidget);
});
testWidgets('an empty mix surfaces the no-tracks snackbar', (tester) async {
final context = await pumpWithService(tester, _OutcomeMusicService(outcome: InstantMixOutcome.empty));
await playInstantMix(context, seed);
await tester.pump();
expect(find.text(t.music.instantMixEmpty), findsOneWidget);
});
testWidgets('a started mix shows no snackbar', (tester) async {
final context = await pumpWithService(tester, _OutcomeMusicService(outcome: InstantMixOutcome.started));
await playInstantMix(context, seed);
await tester.pump();
expect(find.byType(SnackBar), findsNothing);
});
testWidgets('a superseded mix stays silent', (tester) async {
final context = await pumpWithService(tester, _OutcomeMusicService(outcome: InstantMixOutcome.superseded));
await playInstantMix(context, seed);
await tester.pump();
expect(find.byType(SnackBar), findsNothing);
});
}