feat(flutter/track-actions): TrackActionsButton + TrackActionsSheet

The 3-dot trigger and the modal-bottom-sheet menu it opens. 7 actions:
Play next, Add to queue, Like/Unlike, Add to playlist, Go to album,
Go to artist, Hide/Unhide. hideQueueActions: true suppresses the
first two for the Now Playing surface.

Sub-sheets (HideTrackSheet, AddToPlaylistSheet) land in subsequent
commits — this commit imports them speculatively, so CI between this
and the next two commits will fail. Resolved by Tasks 5 + 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 13:41:16 -04:00
parent 2499449c0b
commit 5f239f05a5
3 changed files with 350 additions and 0 deletions
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:minstrel/likes/likes_provider.dart';
import 'package:minstrel/models/quarantine_mine.dart';
import 'package:minstrel/models/track.dart';
import 'package:minstrel/quarantine/quarantine_provider.dart';
import 'package:minstrel/shared/widgets/track_actions/track_actions_sheet.dart';
import 'package:minstrel/theme/theme_data.dart';
const _track = TrackRef(
id: 't1',
title: 'Roygbiv',
albumId: 'a1',
albumTitle: 'Geogaddi',
artistId: 'ar1',
artistName: 'Boards of Canada',
durationSec: 137,
trackNumber: 4,
streamUrl: '',
);
class _StubLiked extends LikedIdsController {
_StubLiked({this.trackIds = const <String>{}});
final Set<String> trackIds;
@override
Future<LikedIds> build() async =>
LikedIds(artists: const {}, albums: const {}, tracks: trackIds);
}
class _StubQuarantine extends MyQuarantineController {
@override
Future<List<QuarantineMineRow>> build() async => const [];
}
Widget _harness({bool hideQueueActions = false, Set<String>? likedTracks}) {
return ProviderScope(
overrides: [
likedIdsProvider
.overrideWith(() => _StubLiked(trackIds: likedTracks ?? const {})),
myQuarantineProvider.overrideWith(() => _StubQuarantine()),
],
child: MaterialApp(
theme: buildThemeData(),
home: Builder(builder: (ctx) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => TrackActionsSheet.show(
ctx,
_track,
hideQueueActions: hideQueueActions,
),
child: const Text('open'),
),
),
);
}),
),
);
}
void main() {
testWidgets('renders all 7 items by default', (t) async {
await t.pumpWidget(_harness());
await t.tap(find.text('open'));
await t.pumpAndSettle();
expect(find.byKey(const Key('track_actions_play_next')), findsOneWidget);
expect(find.byKey(const Key('track_actions_enqueue')), findsOneWidget);
expect(find.byKey(const Key('track_actions_like')), findsOneWidget);
expect(find.byKey(const Key('track_actions_add_to_playlist')), findsOneWidget);
expect(find.byKey(const Key('track_actions_go_to_album')), findsOneWidget);
expect(find.byKey(const Key('track_actions_go_to_artist')), findsOneWidget);
expect(find.byKey(const Key('track_actions_hide')), findsOneWidget);
});
testWidgets('hideQueueActions suppresses Play next + Add to queue', (t) async {
await t.pumpWidget(_harness(hideQueueActions: true));
await t.tap(find.text('open'));
await t.pumpAndSettle();
expect(find.byKey(const Key('track_actions_play_next')), findsNothing);
expect(find.byKey(const Key('track_actions_enqueue')), findsNothing);
expect(find.byKey(const Key('track_actions_like')), findsOneWidget);
});
testWidgets('like label flips to Unlike when track is liked', (t) async {
await t.pumpWidget(_harness(likedTracks: const {'t1'}));
await t.tap(find.text('open'));
await t.pumpAndSettle();
expect(find.text('Unlike'), findsOneWidget);
expect(find.text('Like'), findsNothing);
});
}