Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove test flakiness #181

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ class _AppState extends State<App> with TickerProviderStateMixin {
);
}

@override
void dispose() {
_playerRouteController.dispose();
_drawerController.dispose();
playerRouteControllerInitialized = false;
super.dispose();
}

@override
Widget build(BuildContext context) {
return MediaQueryWrapper(
Expand Down
1 change: 1 addition & 0 deletions test/fakes/fake_just_audio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class MockAudioPlayer implements AudioPlayerPlatform {
Future<DisposeResponse> dispose(DisposeRequest request) async {
_processingState = ProcessingStateMessage.idle;
_broadcastPlaybackEvent();
_playTimer?.cancel();
return DisposeResponse();
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestengly, a side effect I see on many screenshots is that the shadow from the drawer seems to have gone

It's OK, just something I noticed reviewing golden changes

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/artist_route.artist_route.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/artist_route.artist_route.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/persistent_queue_route.album_route.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/persistent_queue_route.album_route.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/persistent_queue_route.playlist_route.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/player_route.player_route.dark.artColor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/player_route.player_route.light.artColor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.results.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.results.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.results_empty.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.results_empty.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.search_suggestions.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.search_suggestions.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/search_route.search_suggestions_empty.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/tabs_route.albums_tab.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/tabs_route.albums_tab.light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/tabs_route.artists_tab.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/golden/goldens/tabs_route.artists_tab.light.png
Binary file modified test/golden/goldens/tabs_route.playlists_tab.dark.png
Binary file modified test/golden/goldens/tabs_route.playlists_tab.light.png
Binary file modified test/golden/goldens/tabs_route.scroll_labels.dark.png
Binary file modified test/golden/goldens/tabs_route.scroll_labels.light.png
Binary file modified test/golden/goldens/tabs_route.songs_tab.dark.png
Binary file modified test/golden/goldens/tabs_route.songs_tab.light.png
Binary file modified test/golden/goldens/tabs_route.sort_feature_dialog.dark.png
Binary file modified test/golden/goldens/tabs_route.sort_feature_dialog.light.png
482 changes: 203 additions & 279 deletions test/golden/routes_golden_test.dart

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions test/logic/models/album_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ void main() {
final song3 = songWith(id: 4, track: '3', title: 'Third Song');
final song4 = songWith(id: 1, track: '4', title: 'Fourth Song');
final song10 = songWith(id: 3, track: '10', title: 'Tenth Song');
await setUpAppTest(() {
await TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [nullSong1, nullSong2, song1, song2, song3, song4, song10];
}, () {
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1, nullSong2]);
});
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1, nullSong2]);
});

test('Sorts tracks with total track count', () async {
Expand All @@ -27,10 +28,11 @@ void main() {
final song3 = songWith(id: 4, track: '3/X', title: 'Third Song');
final song4 = songWith(id: 1, track: '4/0', title: 'Fourth Song');
final song10 = songWith(id: 3, track: '10/10', title: 'Tenth Song');
await setUpAppTest(() {
await TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [nullSong1, song1, song2, song3, song4, song10];
}, () {
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1]);
});
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1]);
});

test('Sorts tracks with spaces in track field', () async {
Expand All @@ -40,9 +42,10 @@ void main() {
final song3 = songWith(id: 4, track: '3 / 10', title: 'Third Song');
final song4 = songWith(id: 1, track: ' 4 / 10 ', title: 'Fourth Song');
final song10 = songWith(id: 3, track: ' 10 /10', title: 'Tenth Song');
await setUpAppTest(() {
await TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [nullSong1, song1, song2, song3, song4, song10];
}, () {
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1]);
});
expect(album.songs, [song1, song2, song3, song4, song10, nullSong1]);
});
}
131 changes: 82 additions & 49 deletions test/logic/player/favorites_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,46 @@ void main() {
final favoriteSong2 = songWith(id: 4, title: 'Song 4', isFavoriteInMediaStore: true);
final favoriteSong3 = songWith(id: 5, title: 'Song 5', isFavoriteInMediaStore: true);

setUp(() async {
await setUpAppTest(() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this is a good API change that we not longer can use setUp between tests?

Copy link
Owner

@nt4f04uNd nt4f04uNd Nov 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue it's not - it reduces the reusability of setup logic

In some cases it might also make that tests utilities are not composable with each other - this is a real case in tests that I had recently, when I made a not very composable API desicion, which put constraints on how tests could be written, so I had to rewrite it at some point after

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the description of the PR I see that you want them to live inside the same zone as other test logic

It this possible to achieve in some other way?

FakeSweyerPluginPlatform.instance.songs = [
notFavoriteSong1,
notFavoriteSong2,
notFavoriteSong3,
favoriteSong1,
favoriteSong2,
favoriteSong3,
];
});
Future<void> setupDefaultFavouriteState(TestWidgetsFlutterBinding binding) async {
await Settings.useMediaStoreForFavoriteSongs.set(false);
await Future.delayed(const Duration(seconds: 1)); // Wait for the listener in FavoriteControl to execute
await binding.pump(const Duration(seconds: 1)); // Wait for the listener in FavoriteControl to execute
await FakeFavoritesControl.instance
.setFavorite(contentTuple: ContentTuple(songs: [favoriteSong1, favoriteSong2, favoriteSong3]), value: true);
await FakeFavoritesControl.instance.setFavorite(
contentTuple: ContentTuple(songs: [notFavoriteSong1, notFavoriteSong2, notFavoriteSong3]), value: false);
});
}

group('MediaStore', () {
testWidgets('Updates the MediaStore correctly when resolving conflicts', (WidgetTester tester) async {
final localFavoriteAndMediaStoreSong = favoriteSong1;
final notFavoriteInBothSong = notFavoriteSong1;
final localFavoriteButNotInMediaStoreKeepSong = notFavoriteSong2;
final localFavoriteButNotInMediaStoreUnFavorSong = notFavoriteSong3;
final mediaStoreFavoriteButNotLocalKeepSong = favoriteSong2;
final mediaStoreFavoriteButNotLocalUnFavorSong = favoriteSong3;
await FakeFavoritesControl.instance.setFavorite(
contentTuple: ContentTuple(
songs: [localFavoriteButNotInMediaStoreKeepSong, localFavoriteButNotInMediaStoreUnFavorSong],
),
value: true,
);
await FakeFavoritesControl.instance.setFavorite(
contentTuple: ContentTuple(
songs: [mediaStoreFavoriteButNotLocalKeepSong, mediaStoreFavoriteButNotLocalUnFavorSong],
),
value: false,
);
await tester.runAppTest(() async {
await tester.runAppTest(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [
notFavoriteSong1,
notFavoriteSong2,
notFavoriteSong3,
favoriteSong1,
favoriteSong2,
favoriteSong3,
];
}, () async {
await setupDefaultFavouriteState(tester.binding);
final localFavoriteAndMediaStoreSong = favoriteSong1;
final notFavoriteInBothSong = notFavoriteSong1;
final localFavoriteButNotInMediaStoreKeepSong = notFavoriteSong2;
final localFavoriteButNotInMediaStoreUnFavorSong = notFavoriteSong3;
final mediaStoreFavoriteButNotLocalKeepSong = favoriteSong2;
final mediaStoreFavoriteButNotLocalUnFavorSong = favoriteSong3;
await FakeFavoritesControl.instance.setFavorite(
contentTuple: ContentTuple(
songs: [localFavoriteButNotInMediaStoreKeepSong, localFavoriteButNotInMediaStoreUnFavorSong],
),
value: true,
);
await FakeFavoritesControl.instance.setFavorite(
contentTuple: ContentTuple(
songs: [mediaStoreFavoriteButNotLocalKeepSong, mediaStoreFavoriteButNotLocalUnFavorSong],
),
value: false,
);
await Settings.useMediaStoreForFavoriteSongs.set(true);
await tester.pumpAndSettle();
Finder findInDialog(Finder finder) => find.descendant(of: find.byType(AlertDialog), matching: finder);
Expand Down Expand Up @@ -82,17 +82,37 @@ void main() {

testWidgets("When switching to MediaStore, doesn't show resolve conflict dialog with no conflicts",
(WidgetTester tester) async {
await tester.runAppTest(() async {
await tester.runAppTest(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [
notFavoriteSong1,
notFavoriteSong2,
notFavoriteSong3,
favoriteSong1,
favoriteSong2,
favoriteSong3,
];
}, () async {
await setupDefaultFavouriteState(tester.binding);
await Settings.useMediaStoreForFavoriteSongs.set(true);
await tester.pumpAndSettle();
expect(find.text(l10n.resolveConflict), findsNothing);
});
});

testWidgets('Allows to cancel when resolving conflicts', (WidgetTester tester) async {
await FakeFavoritesControl.instance
.setFavorite(contentTuple: ContentTuple(songs: [notFavoriteSong1]), value: true);
await tester.runAppTest(() async {
await tester.runAppTest(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [
notFavoriteSong1,
notFavoriteSong2,
notFavoriteSong3,
favoriteSong1,
favoriteSong2,
favoriteSong3,
];
}, () async {
await setupDefaultFavouriteState(tester.binding);
await FakeFavoritesControl.instance
.setFavorite(contentTuple: ContentTuple(songs: [notFavoriteSong1]), value: true);
await Settings.useMediaStoreForFavoriteSongs.set(true);
await tester.pumpAndSettle();
expect(find.text(l10n.resolveConflict), findsOneWidget);
Expand All @@ -103,19 +123,32 @@ void main() {
});
});

testWidgets('Keeps favorites when switching form MediaStore to local', (WidgetTester tester) async {
final favorSong = notFavoriteSong1;
final unFavorSong = favoriteSong1;
await Settings.useMediaStoreForFavoriteSongs.set(true);
await tester.pumpAndSettle();
await FakeFavoritesControl.instance.setFavorite(contentTuple: ContentTuple(songs: [favorSong]), value: true);
await FakeFavoritesControl.instance.setFavorite(contentTuple: ContentTuple(songs: [unFavorSong]), value: false);
await Settings.useMediaStoreForFavoriteSongs.set(false);
await tester.pumpAndSettle();
expect(FakeFavoritesControl.instance.isFavorite(unFavorSong), false);
expect(FakeFavoritesControl.instance.isFavorite(notFavoriteSong2), false);
expect(FakeFavoritesControl.instance.isFavorite(favorSong), true);
expect(FakeFavoritesControl.instance.isFavorite(favoriteSong2), true);
test('Keeps favorites when switching form MediaStore to local', () async {
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAppTestWithoutUi(initialization: () {
FakeSweyerPluginPlatform.instance.songs = [
notFavoriteSong1,
notFavoriteSong2,
notFavoriteSong3,
favoriteSong1,
favoriteSong2,
favoriteSong3,
];
}, () async {
await setupDefaultFavouriteState(binding);
final favorSong = notFavoriteSong1;
final unFavorSong = favoriteSong1;
await Settings.useMediaStoreForFavoriteSongs.set(true);
await binding.pump();
await FakeFavoritesControl.instance.setFavorite(contentTuple: ContentTuple(songs: [favorSong]), value: true);
await FakeFavoritesControl.instance.setFavorite(contentTuple: ContentTuple(songs: [unFavorSong]), value: false);
await Settings.useMediaStoreForFavoriteSongs.set(false);
await binding.pump();
expect(FakeFavoritesControl.instance.isFavorite(unFavorSong), false);
expect(FakeFavoritesControl.instance.isFavorite(notFavoriteSong2), false);
expect(FakeFavoritesControl.instance.isFavorite(favorSong), true);
expect(FakeFavoritesControl.instance.isFavorite(favoriteSong2), true);
});
});
});
}
137 changes: 70 additions & 67 deletions test/logic/player/music_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,87 @@ import 'package:rxdart/rxdart.dart';
import '../../test.dart';

void main() {
setUp(() async {
MusicPlayer.handler?.playbackState.add(PlaybackState());
await setUpAppTest();
});

tearDown(() async {
await MusicPlayer.instance.dispose();
});

test('Player can pause', () async {
var playFutureCompleted = false;
MusicPlayer.instance.play().whenComplete(() => playFutureCompleted = true);
await pumpEventQueue();
expect(MusicPlayer.instance.playing, true);
await MusicPlayer.instance.pause();
await pumpEventQueue();
expect(playFutureCompleted, true);
expect(MusicPlayer.instance.playing, false);
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAppTestWithoutUi(() async {
var playFutureCompleted = false;
MusicPlayer.instance.play().whenComplete(() => playFutureCompleted = true);
await binding.pump();
expect(MusicPlayer.instance.playing, true);
await MusicPlayer.instance.pause();
await binding.pump();
expect(playFutureCompleted, true);
expect(MusicPlayer.instance.playing, false);
});
});

group('Player notification', () {
test('Is updated when playing', () async {
MusicPlayer.instance.play();
await pumpEventQueue();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(playbackState.playing, true);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_pause', label: l10n.pause, action: 'pause'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAppTestWithoutUi(() async {
MusicPlayer.instance.play();
await binding.pump();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(playbackState.playing, true);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_pause', label: l10n.pause, action: 'pause'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
});
});

test('Is updated when paused', () async {
MusicPlayer.instance.play();
await pumpEventQueue();
await MusicPlayer.instance.pause();
await pumpEventQueue();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(MusicPlayer.instance.playing, false);
expect(playbackState.playing, false);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_play_arrow', label: l10n.play, action: 'play'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAppTestWithoutUi(() async {
MusicPlayer.instance.play();
await binding.pump();
await MusicPlayer.instance.pause();
await binding.pump();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(MusicPlayer.instance.playing, false);
expect(playbackState.playing, false);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_play_arrow', label: l10n.play, action: 'play'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
});
});

test('Is updated when stopped', () async {
MusicPlayer.instance.play();
await pumpEventQueue();
await MusicPlayer.handler!.onNotificationAction('stop');
await pumpEventQueue();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(playbackState.playing, false);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_play_arrow', label: l10n.play, action: 'play'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.runAppTestWithoutUi(() async {
MusicPlayer.instance.play();
await binding.pump();
await MusicPlayer.handler!.onNotificationAction('stop');
await binding.pump();
final playbackState = MusicPlayer.handler!.playbackState.value!;
expect(playbackState.playing, false);
expect(playbackState.processingState, AudioProcessingState.ready);
expect(
playbackState.controls.map((control) => control.toString()),
[
MediaControl(androidIcon: 'drawable/round_loop', label: l10n.loopOff, action: 'loop_off'),
MediaControl(androidIcon: 'drawable/round_skip_previous', label: l10n.previous, action: 'play_prev'),
MediaControl(androidIcon: 'drawable/round_play_arrow', label: l10n.play, action: 'play'),
MediaControl(androidIcon: 'drawable/round_skip_next', label: l10n.next, action: 'play_next'),
MediaControl(androidIcon: 'drawable/round_stop', label: l10n.stop, action: 'stop'),
].map((control) => control.toString()),
);
});
});
});
}
Loading