diff --git a/lib/airplane_entertainment_system/view/airplane_entertainment_system_screen.dart b/lib/airplane_entertainment_system/view/airplane_entertainment_system_screen.dart index 73058aa..fd97353 100644 --- a/lib/airplane_entertainment_system/view/airplane_entertainment_system_screen.dart +++ b/lib/airplane_entertainment_system/view/airplane_entertainment_system_screen.dart @@ -32,6 +32,7 @@ class AirplaneEntertainmentSystemScreen extends StatelessWidget { } class AirplaneEntertainmentSystemView extends StatefulWidget { + @visibleForTesting const AirplaneEntertainmentSystemView({super.key}); @override diff --git a/lib/airplane_entertainment_system/widgets/mute_button.dart b/lib/airplane_entertainment_system/widgets/mute_button.dart new file mode 100644 index 0000000..364bbfe --- /dev/null +++ b/lib/airplane_entertainment_system/widgets/mute_button.dart @@ -0,0 +1,19 @@ +import 'package:airplane_entertainment_system/music_player/music_player.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MuteButton extends StatelessWidget { + const MuteButton({super.key}); + + @override + Widget build(BuildContext context) { + final mute = context.select((MusicPlayerCubit cubit) => cubit.state.mute); + + return IconButton( + onPressed: () { + context.read().toggleMute(); + }, + icon: Icon(mute ? Icons.volume_off : Icons.volume_up), + ); + } +} diff --git a/lib/airplane_entertainment_system/widgets/top_button_bar.dart b/lib/airplane_entertainment_system/widgets/top_button_bar.dart index f8ea648..9cc9437 100644 --- a/lib/airplane_entertainment_system/widgets/top_button_bar.dart +++ b/lib/airplane_entertainment_system/widgets/top_button_bar.dart @@ -1,5 +1,5 @@ +import 'package:airplane_entertainment_system/airplane_entertainment_system/airplane_entertainment_system.dart'; import 'package:airplane_entertainment_system/generated/assets.gen.dart'; -import 'package:airplane_entertainment_system/l10n/l10n.dart'; import 'package:flutter/material.dart'; class TopButtonBar extends StatelessWidget { @@ -17,50 +17,7 @@ class TopButtonBar extends StatelessWidget { children: [ Assets.vgvLogo.image(width: 40, height: 40), const SizedBox(width: 24), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.power_settings_new), - ), - Container( - width: 1, - height: 24, - margin: const EdgeInsets.symmetric(horizontal: 8), - color: Colors.white.withOpacity(0.3), - ), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.brightness_7), - ), - Container( - width: 1, - height: 24, - margin: const EdgeInsets.symmetric(horizontal: 8), - color: Colors.white.withOpacity(0.3), - ), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.volume_up), - ), - const Spacer(), - ElevatedButton.icon( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade800, - padding: const EdgeInsets.all(16), - textStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 12, - letterSpacing: 1.2, - ), - ), - icon: const Icon(Icons.support, color: Colors.white), - label: Text( - context.l10n.assistButton, - style: const TextStyle( - color: Colors.white, - ), - ), - ), + const MuteButton(), ], ), ), diff --git a/lib/airplane_entertainment_system/widgets/widgets.dart b/lib/airplane_entertainment_system/widgets/widgets.dart index 97ecb63..239d5ed 100644 --- a/lib/airplane_entertainment_system/widgets/widgets.dart +++ b/lib/airplane_entertainment_system/widgets/widgets.dart @@ -1,2 +1,3 @@ +export 'mute_button.dart'; export 'system_background.dart'; export 'top_button_bar.dart'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e23d074..a3bbaf5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -19,7 +19,6 @@ "songs": "songs", "welcomeMessage": "Welcome on board", "welcomeSubtitle": "Lunch will be served in\n10 minutes", - "assistButton": "ASSIST", "clear": "Clear", "@clear": { "description": "The label shown when weather is clear." diff --git a/lib/music_player/cubit/music_player_cubit.dart b/lib/music_player/cubit/music_player_cubit.dart index 3134ef4..ac29b7c 100644 --- a/lib/music_player/cubit/music_player_cubit.dart +++ b/lib/music_player/cubit/music_player_cubit.dart @@ -18,6 +18,8 @@ class MusicPlayerCubit extends Cubit { _player.onPlayerStateChanged.listen(_onIsPlayingChanged); _progressSubscription = _player.onPositionChanged.listen(_onProgressChanged); + + _player.setVolume(1); } final MusicRepository _musicRepository; @@ -155,6 +157,12 @@ class MusicPlayerCubit extends Cubit { } } + void toggleMute() { + final mute = !state.mute; + _player.setVolume(mute ? 0 : 1); + emit(state.copyWith(mute: mute)); + } + @override Future close() { _isPlayingSubscription.cancel(); diff --git a/lib/music_player/cubit/music_player_state.dart b/lib/music_player/cubit/music_player_state.dart index 34eceee..8ef668c 100644 --- a/lib/music_player/cubit/music_player_state.dart +++ b/lib/music_player/cubit/music_player_state.dart @@ -9,6 +9,7 @@ class MusicPlayerState extends Equatable { this.isPlaying = false, this.isLoop = false, this.shuffleIndexes = const [], + this.mute = false, }); final List tracks; @@ -18,6 +19,7 @@ class MusicPlayerState extends Equatable { final bool isPlaying; final bool isLoop; final List shuffleIndexes; + final bool mute; bool get isShuffle => shuffleIndexes.isNotEmpty; @@ -34,6 +36,7 @@ class MusicPlayerState extends Equatable { progress, isLoop, shuffleIndexes, + mute, ]; MusicPlayerState copyWith({ @@ -44,6 +47,7 @@ class MusicPlayerState extends Equatable { bool? isPlaying, bool? isLoop, List? shuffleIndexes, + bool? mute, }) { return MusicPlayerState( tracks: tracks ?? this.tracks, @@ -53,6 +57,7 @@ class MusicPlayerState extends Equatable { progress: progress ?? this.progress, isLoop: isLoop ?? this.isLoop, shuffleIndexes: shuffleIndexes ?? this.shuffleIndexes, + mute: mute ?? this.mute, ); } } diff --git a/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart b/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart index 89df0b9..3ad262b 100644 --- a/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart +++ b/test/airplane_entertainment_system/view/airplane_entertainment_system_screen_test.dart @@ -2,8 +2,11 @@ import 'package:aes_ui/aes_ui.dart'; import 'package:airplane_entertainment_system/airplane_entertainment_system/airplane_entertainment_system.dart'; import 'package:airplane_entertainment_system/music_player/music_player.dart'; import 'package:airplane_entertainment_system/overview/overview.dart'; +import 'package:airplane_entertainment_system/weather/weather.dart'; import 'package:audioplayers/audioplayers.dart'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:music_repository/music_repository.dart'; @@ -13,6 +16,14 @@ import '../../helpers/helpers.dart'; class _MockAudioCache extends Mock implements AudioCache {} +class _MockAudioPlayer extends Mock implements AudioPlayer {} + +class _MockWeatherBloc extends MockBloc + implements WeatherBloc {} + +class _MockMusicPlayerCubit extends MockCubit + implements MusicPlayerCubit {} + void main() { group('$AirplaneEntertainmentSystemScreen', () { late WeatherRepository weatherRepository; @@ -27,7 +38,7 @@ void main() { musicRepository = MockMusicRepository(); when(musicRepository.getTracks).thenReturn(const []); - audioPlayer = MockAudioPlayer(); + audioPlayer = _MockAudioPlayer(); when(() => audioPlayer.onPositionChanged) .thenAnswer((_) => const Stream.empty()); when(() => audioPlayer.onPlayerStateChanged) @@ -36,14 +47,29 @@ void main() { final audioCache = _MockAudioCache(); when(() => audioPlayer.audioCache).thenReturn(audioCache); + when(() => audioPlayer.setVolume(any())).thenAnswer((_) async {}); }); - testWidgets('shows $AesNavigationRail on large screens', (tester) async { - await tester.binding.setSurfaceSize(const Size(1600, 1200)); + testWidgets('finds one AirplaneEntertainmentSystemView Widget', + (tester) async { await tester.pumpApp( const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.large, + layout: AesLayoutData.small, + musicRepository: musicRepository, weatherRepository: weatherRepository, + audioPlayer: audioPlayer, + ); + + expect(find.byType(AirplaneEntertainmentSystemView), findsOneWidget); + }); + }); + + group('$AirplaneEntertainmentSystemView', () { + testWidgets('shows $AesNavigationRail on large screens', (tester) async { + await tester.binding.setSurfaceSize(const Size(1600, 1200)); + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.large, ); expect(find.byType(AesNavigationRail), findsOneWidget); @@ -51,52 +77,45 @@ void main() { testWidgets('shows $AesBottomNavigationBar on small screens', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.small, - weatherRepository: weatherRepository, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.small, ); expect(find.byType(AesBottomNavigationBar), findsOneWidget); }); testWidgets('shows TopButtonBar', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.small, - weatherRepository: weatherRepository, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.small, ); expect(find.byType(TopButtonBar), findsOneWidget); }); testWidgets('contains background', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.small, - weatherRepository: weatherRepository, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.small, ); expect(find.byType(SystemBackground), findsOneWidget); }); testWidgets('shows OverviewPage initially', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.small, - weatherRepository: weatherRepository, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.small, ); expect(find.byType(OverviewPage), findsOneWidget); }); testWidgets('shows MusicPlayerPage when icon is selected', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: AesLayoutData.small, - weatherRepository: weatherRepository, - musicRepository: musicRepository, - audioPlayer: audioPlayer, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + AesLayoutData.small, ); await tester.tap(find.byIcon(Icons.music_note)); @@ -110,12 +129,9 @@ void main() { testWidgets( 'shows $OverviewPage when icon is ' 'selected for $layout layout', (tester) async { - await tester.pumpApp( - const AirplaneEntertainmentSystemScreen(), - layout: layout, - weatherRepository: weatherRepository, - musicRepository: musicRepository, - audioPlayer: audioPlayer, + await tester.pumpSubject( + const AirplaneEntertainmentSystemView(), + layout, ); await tester.tap(find.byIcon(Icons.music_note)); @@ -129,3 +145,24 @@ void main() { } }); } + +extension on WidgetTester { + Future pumpSubject(Widget widget, AesLayoutData layout) { + final MusicPlayerCubit musicPlayerCubit = _MockMusicPlayerCubit(); + when(() => musicPlayerCubit.state).thenReturn(const MusicPlayerState()); + + final WeatherBloc weatherBloc = _MockWeatherBloc(); + when(() => weatherBloc.state).thenReturn(const WeatherState()); + + return pumpApp( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: musicPlayerCubit), + BlocProvider.value(value: weatherBloc), + ], + child: widget, + ), + layout: layout, + ); + } +} diff --git a/test/airplane_entertainment_system/widgets/mute_button_test.dart b/test/airplane_entertainment_system/widgets/mute_button_test.dart new file mode 100644 index 0000000..51768d4 --- /dev/null +++ b/test/airplane_entertainment_system/widgets/mute_button_test.dart @@ -0,0 +1,65 @@ +// ignore_for_file: prefer_const_constructors, avoid_redundant_argument_values + +import 'package:airplane_entertainment_system/airplane_entertainment_system/airplane_entertainment_system.dart'; +import 'package:airplane_entertainment_system/music_player/music_player.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockMusicPlayerCubit extends MockCubit + implements MusicPlayerCubit {} + +void main() { + group('$MuteButton', () { + testWidgets('finds one MuteButton Widget', (tester) async { + final MusicPlayerCubit cubit = _MockMusicPlayerCubit(); + when(() => cubit.state).thenReturn(const MusicPlayerState()); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: cubit, + child: MuteButton(), + ), + ), + ), + ); + + expect(find.byType(MuteButton), findsOneWidget); + }); + + testWidgets('toggles mute when pressed', (tester) async { + final MusicPlayerCubit cubit = _MockMusicPlayerCubit(); + + whenListen( + cubit, + Stream.fromIterable( + [ + const MusicPlayerState(mute: true), + ], + ), + initialState: const MusicPlayerState(mute: false), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider.value( + value: cubit, + child: MuteButton(), + ), + ), + ), + ); + + await tester.tap(find.byType(IconButton)); + verify(cubit.toggleMute).called(1); + + await tester.pump(); + expect(find.byIcon(Icons.volume_off), findsOneWidget); + }); + }); +} diff --git a/test/airplane_entertainment_system/widgets/top_button_bar_test.dart b/test/airplane_entertainment_system/widgets/top_button_bar_test.dart index c6207a2..755a775 100644 --- a/test/airplane_entertainment_system/widgets/top_button_bar_test.dart +++ b/test/airplane_entertainment_system/widgets/top_button_bar_test.dart @@ -1,53 +1,39 @@ import 'package:airplane_entertainment_system/airplane_entertainment_system/airplane_entertainment_system.dart'; +import 'package:airplane_entertainment_system/music_player/music_player.dart'; +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; -void main() { - group('TopButtonBar', () { - testWidgets('contains power button', (tester) async { - await tester.pumpApp( - const Scaffold( - body: TopButtonBar(), - ), - ); - - expect(find.byIcon(Icons.power_settings_new), findsOneWidget); - await tester.tap(find.byIcon(Icons.power_settings_new)); - }); - - testWidgets('contains brightness button', (tester) async { - await tester.pumpApp( - const Scaffold( - body: TopButtonBar(), - ), - ); - - expect(find.byIcon(Icons.brightness_7), findsOneWidget); - await tester.tap(find.byIcon(Icons.brightness_7)); - }); +class _MockMusicPlayerCubit extends MockCubit + implements MusicPlayerCubit {} +void main() { + group('$TopButtonBar', () { testWidgets('contains volume button', (tester) async { - await tester.pumpApp( - const Scaffold( - body: TopButtonBar(), - ), - ); + await tester.pumpSubject(); expect(find.byIcon(Icons.volume_up), findsOneWidget); await tester.tap(find.byIcon(Icons.volume_up)); }); + }); +} - testWidgets('contains assist button', (tester) async { - await tester.pumpApp( - const Scaffold( +extension on WidgetTester { + Future pumpSubject() { + final MusicPlayerCubit musicPlayerCubit = _MockMusicPlayerCubit(); + when(() => musicPlayerCubit.state).thenReturn(const MusicPlayerState()); + + return pumpApp( + BlocProvider.value( + value: musicPlayerCubit, + child: const Scaffold( body: TopButtonBar(), ), - ); - - expect(find.byIcon(Icons.support), findsOneWidget); - await tester.tap(find.byIcon(Icons.support)); - }); - }); + ), + ); + } } diff --git a/test/music_player/cubit/music_player_cubit_test.dart b/test/music_player/cubit/music_player_cubit_test.dart index 581a03b..eee16ce 100644 --- a/test/music_player/cubit/music_player_cubit_test.dart +++ b/test/music_player/cubit/music_player_cubit_test.dart @@ -47,6 +47,7 @@ void main() { audioCache = _MockAudioCache(); when(() => audioPlayer.audioCache).thenReturn(audioCache); + when(() => audioPlayer.setVolume(any())).thenAnswer((_) async {}); }); MusicPlayerCubit build() => MusicPlayerCubit( @@ -506,5 +507,36 @@ void main() { ], ); }); + + group('toggle mute', () { + blocTest( + 'updates [mute] to true when it is false', + build: build, + act: (cubit) => cubit.toggleMute(), + expect: () => [ + isA().having( + (s) => s.mute, + 'mute', + isTrue, + ), + ], + ); + + blocTest( + 'updates [mute] to false when it is true', + build: build, + seed: () => const MusicPlayerState( + mute: true, + ), + act: (cubit) => cubit.toggleMute(), + expect: () => [ + isA().having( + (s) => s.mute, + 'mute', + isFalse, + ), + ], + ); + }); }); }