diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index c1e5ebe8..d939b1db 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -395,7 +395,7 @@ class MediaContextMenuState extends State { // reachable (capabilities stay truthy for offline servers). if (itemServerOnline && (mediaClient?.capabilities.instantMix ?? false)) { menuActions.add( - _MenuAction(value: 'music_instant_mix', icon: Symbols.instant_mix_rounded, label: t.music.instantMix), + _MenuAction(value: 'music_instant_mix', icon: Symbols.wand_stars_rounded, label: t.music.instantMix), ); } diff --git a/lib/widgets/music/music_actions.dart b/lib/widgets/music/music_actions.dart index 6c1668ed..bbb24f83 100644 --- a/lib/widgets/music/music_actions.dart +++ b/lib/widgets/music/music_actions.dart @@ -33,7 +33,7 @@ List buildMusicActions({ if (onInstantMix != null) FocusableAction( debugLabel: 'music_instant_mix', - icon: Symbols.instant_mix_rounded, + icon: Symbols.wand_stars_rounded, tooltip: t.music.instantMix, onPressed: onInstantMix, ), diff --git a/test/widgets/media_context_menu_test.dart b/test/widgets/media_context_menu_test.dart index a2fe2927..7c2b5cbb 100644 --- a/test/widgets/media_context_menu_test.dart +++ b/test/widgets/media_context_menu_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:plezy/connection/connection.dart'; import 'package:plezy/database/app_database.dart'; import 'package:plezy/i18n/strings.g.dart'; @@ -556,6 +557,42 @@ void main() { expect(find.text('picker target'), findsOneWidget); }); + testWidgets('the music menu gives Instant Mix an icon it shares with no sibling row', (tester) async { + final track = testMediaItem( + id: 'track-1', + backend: MediaBackend.jellyfin, + kind: MediaKind.track, + title: 'Track', + parentId: 'album-1', + parentTitle: 'Album', + grandparentId: 'artist-1', + grandparentTitle: 'Artist', + serverId: 'srv-1', + ); + final harness = await _pumpSiblingMusicMenu(tester, item: track, relatedItems: const []); + + harness.menuKey.currentState!.showContextMenu(tester.element(find.text('mini-player menu target'))); + await tester.pumpAndSettle(); + + expect(find.text(t.music.instantMix), findsOneWidget); + expect(find.byIcon(Symbols.wand_stars_rounded), findsOneWidget); + + // #1629: the fader glyph reads as an equalizer, and the neighbouring + // rows must stay tellable apart at a glance on a TV. + expect(find.byIcon(Symbols.instant_mix_rounded), findsNothing); + expect(find.byIcon(Symbols.tune_rounded), findsNothing); + final musicIcons = tester + .widgetList(find.byType(Icon)) + .map((icon) => icon.icon) + .whereType() + .toList(); + expect( + musicIcons.where((icon) => icon == Symbols.wand_stars_rounded), + hasLength(1), + reason: 'Instant Mix must not reuse the glyph of Play, Play next, Add to queue, or Go to artist', + ); + }); + testWidgets('track album action uses the profile navigator from the sibling menu overlay', (tester) async { final track = testMediaItem( id: 'track-1', diff --git a/test/widgets/music/music_actions_test.dart b/test/widgets/music/music_actions_test.dart new file mode 100644 index 00000000..88625aa5 --- /dev/null +++ b/test/widgets/music/music_actions_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:plezy/focus/focusable_action_bar.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/theme/mono_theme.dart'; +import 'package:plezy/widgets/music/music_actions.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() => LocaleSettings.setLocaleSync(AppLocale.en)); + + testWidgets('Instant Mix renders a distinct icon and runs its callback when the server supports it', (tester) async { + var mixes = 0; + + await tester.pumpWidget(_wrap(buildMusicActions(onPlay: () {}, onShuffle: () {}, onInstantMix: () => mixes++))); + + final instantMix = find.byIcon(Symbols.wand_stars_rounded); + expect(instantMix, findsOneWidget); + expect( + find.ancestor(of: instantMix, matching: find.byTooltip(t.music.instantMix)), + findsOneWidget, + reason: 'the icon is the only affordance on TV, so it must carry the Instant Mix tooltip', + ); + + // #1629: the fader glyph reads as an equalizer in a music context, and is + // the vertical twin of the video player's settings icon. + expect(find.byIcon(Symbols.instant_mix_rounded), findsNothing); + expect(find.byIcon(Symbols.tune_rounded), findsNothing); + // It must also stay distinguishable from the shuffle button beside it. + expect(find.byIcon(Symbols.shuffle_rounded), findsOneWidget); + + await tester.tap(instantMix); + await tester.pump(); + + expect(mixes, 1); + }); + + testWidgets('Instant Mix is absent when the server lacks the capability', (tester) async { + await tester.pumpWidget(_wrap(buildMusicActions(onPlay: () {}, onShuffle: () {}))); + + expect(find.byIcon(Symbols.wand_stars_rounded), findsNothing); + expect(find.byTooltip(t.music.instantMix), findsNothing); + expect(find.byIcon(Symbols.shuffle_rounded), findsOneWidget); + expect(find.text(t.common.play), findsOneWidget); + }); +} + +Widget _wrap(List actions) { + return TranslationProvider( + child: MaterialApp( + theme: monoTheme(dark: true), + home: Scaffold( + body: Center(child: FocusableActionBar(actions: actions)), + ), + ), + ); +}