From cdd66488e7e25e98189789178b3903239d1c1f24 Mon Sep 17 00:00:00 2001 From: Augsorn Chanklad Date: Wed, 13 Mar 2024 22:12:58 +0700 Subject: [PATCH] Separate logic song detail screen pv and without pv --- .../song_detail_screen.dart | 33 +- .../widgets/song_detail_content.dart | 3 - .../widgets/song_detail_pv_player.dart | 2 +- .../widgets/song_detail_with_pv.dart | 46 +++ .../widgets/song_detail_without_pv.dart | 45 +++ .../song_detail_screen_test.dart | 381 +++++++++--------- test/src/features/songs/song_robot.dart | 1 + 7 files changed, 310 insertions(+), 201 deletions(-) create mode 100644 lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_with_pv.dart create mode 100644 lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_without_pv.dart diff --git a/lib/src/features/songs/presentation/song_detail_screen/song_detail_screen.dart b/lib/src/features/songs/presentation/song_detail_screen/song_detail_screen.dart index 2c23e87d..5e50757f 100644 --- a/lib/src/features/songs/presentation/song_detail_screen/song_detail_screen.dart +++ b/lib/src/features/songs/presentation/song_detail_screen/song_detail_screen.dart @@ -4,14 +4,30 @@ import 'package:vocadb_app/src/common_widgets/async_value_widget.dart'; import 'package:vocadb_app/src/features/home/presentation/app_bar/global_app_bar.dart'; import 'package:vocadb_app/src/features/songs/domain/song.dart'; import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/song_detail_screen_controller.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_albums_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_artists_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_button_bar.dart'; import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_content.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_derived_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_info_section.dart'; import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_lyrics_content.dart'; import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pvs_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_related_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_tags_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_web_links_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_with_pv.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_without_pv.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_hero_image.dart'; class SongDetailScreen extends ConsumerStatefulWidget { - const SongDetailScreen({super.key, required this.song}); + + const SongDetailScreen({super.key, required this.song, this.pvPlayerWidget}); + final Song song; + final Widget? pvPlayerWidget; + /// Keys for test static const tagsKey = Key('tags-key'); static const artistsKey = Key('artists-key'); @@ -30,14 +46,17 @@ class _SongDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(songDetailScreenControllerProvider(widget.song.id)); - + return AsyncValueWidget( value: state.song, - data: (song) => SafeArea(child: Scaffold( - appBar: const GlobalAppBar(title: Text('Song Detail')), - body: - (song.hasYoutubePV)? SongDetailPVPlayer(song: song) : const Text('No PV!!'), - ),), + data: (song) => SafeArea( + child: Scaffold( + appBar: GlobalAppBar(title: Text(song.name ?? 'Song detail')), + body: (song.hasYoutubePV) + ? SongDetailWithPV(song: song, pvPlayerWidget: widget.pvPlayerWidget, onTapLyricButton: () {},) + : SongDetailWithoutPV(song: song, onTapLyricButton: () {},), + ), + ), ); } } diff --git a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_content.dart b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_content.dart index 3c127a5f..91d8bf1c 100644 --- a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_content.dart +++ b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_content.dart @@ -26,9 +26,6 @@ class SongDetailContent extends StatelessWidget { child: ListView( key: SongDetailScreen.songDetailScrollKey, children: [ - SongHeroImage( - imageUrl: song.imageUrl!, - ), SongDetailButtonBar(song: song, onTapLyricButton: onTapLyricButton), SongDetailInfoSection(song: song), SongDetailTagsSection(song: song), diff --git a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart index 35d21188..5af62b00 100644 --- a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart +++ b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart @@ -23,7 +23,7 @@ class _SongDetailPVPlayerState extends ConsumerState { initialVideoId: YoutubePlayer.convertUrlToId(widget.song.youtubeUrl!)!, flags: const YoutubePlayerFlags( mute: false, - autoPlay: true, + autoPlay: false, ), ); super.initState(); diff --git a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_with_pv.dart b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_with_pv.dart new file mode 100644 index 00000000..e634e965 --- /dev/null +++ b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_with_pv.dart @@ -0,0 +1,46 @@ + +import 'package:flutter/material.dart'; +import 'package:vocadb_app/src/features/songs/domain/song.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_albums_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_artists_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_button_bar.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_derived_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_info_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pvs_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_related_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_tags_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_web_links_section.dart'; + +class SongDetailWithPV extends StatelessWidget { + final Song song; + + final Widget? pvPlayerWidget; + + final VoidCallback? onTapLyricButton; + + const SongDetailWithPV({super.key, required this.song, this.pvPlayerWidget, this.onTapLyricButton}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + pvPlayerWidget ?? SongDetailPVPlayer(song: song), + Expanded( + child: ListView( + children: [ + SongDetailButtonBar(song: song, onTapLyricButton: onTapLyricButton), + SongDetailInfoSection(song: song), + SongDetailTagsSection(song: song), + SongDetailArtistsSection(song: song), + SongDetailPVsSection(song: song), + SongDetailAlbumsSection(song: song), + SongDetailDerivedSongsSection(song: song), + SongDetailRelatedSection(song: song), + SongDetailWebLinksSection(song: song), + ], + ), + ), + ]); + } +} + diff --git a/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_without_pv.dart b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_without_pv.dart new file mode 100644 index 00000000..63a14ad1 --- /dev/null +++ b/lib/src/features/songs/presentation/song_detail_screen/widgets/song_detail_without_pv.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:vocadb_app/src/features/songs/domain/song.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_albums_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_artists_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_button_bar.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_derived_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_info_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pv_player.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_pvs_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_related_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_tags_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_detail_web_links_section.dart'; +import 'package:vocadb_app/src/features/songs/presentation/song_detail_screen/widgets/song_hero_image.dart'; + +class SongDetailWithoutPV extends StatelessWidget { + final Song song; + + final VoidCallback? onTapLyricButton; + + const SongDetailWithoutPV({super.key, required this.song, this.onTapLyricButton}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + SongHeroImage( + imageUrl: song.imageUrl!, + ), + Expanded( + child: ListView( + children: [ + SongDetailButtonBar(song: song, onTapLyricButton: onTapLyricButton), + SongDetailInfoSection(song: song), + SongDetailTagsSection(song: song), + SongDetailArtistsSection(song: song), + SongDetailPVsSection(song: song), + SongDetailAlbumsSection(song: song), + SongDetailDerivedSongsSection(song: song), + SongDetailRelatedSection(song: song), + SongDetailWebLinksSection(song: song), + ], + ), + ), + ]); + } +} \ No newline at end of file diff --git a/test/src/features/songs/presentation/song_detail_screen/song_detail_screen_test.dart b/test/src/features/songs/presentation/song_detail_screen/song_detail_screen_test.dart index 3d55f34a..b232b937 100644 --- a/test/src/features/songs/presentation/song_detail_screen/song_detail_screen_test.dart +++ b/test/src/features/songs/presentation/song_detail_screen/song_detail_screen_test.dart @@ -10,194 +10,195 @@ import '../../../../mocks.dart'; import '../../song_robot.dart'; void main() { -// testWidgets('song detail screen with fetch song detail success', -// (tester) async { -// /// Setup -// final r = SongRobot(tester); -// final songRepository = MockSongRepository(); -// final authRepository = MockAuthRepository(); -// final song = kFakeSongDetail; - -// /// Mock -// callFetchSongId() => -// songRepository.fetchSongId(song.id, lang: PreferredLang.Default.name); -// callFetchSongDerived() => songRepository.fetchSongsDerived(song.id, -// lang: PreferredLang.Default.name); -// callFetchSongRelated() => songRepository.fetchSongsRelated(song.id, -// lang: PreferredLang.Default.name); - -// when(callFetchSongId).thenAnswer((_) => Future.value(song)); -// when(callFetchSongDerived).thenAnswer((_) => Future.value([ -// const Song(id: 1, name: 'Song_A'), -// const Song(id: 2, name: 'Song_B'), -// ])); - -// when(callFetchSongRelated).thenAnswer( -// (_) => Future.value( -// const SongRelated(likeMatches: [ -// Song(id: 3, name: 'Song_Related_A'), -// Song(id: 4, name: 'Song_Related_B'), -// ]), -// ), -// ); - -// /// Pump screen -// await r.pumpSongDetailScreen( -// songRepository: songRepository, -// authRepository: authRepository, -// songId: song.id, -// ); - -// // Verify -// verify(callFetchSongId).called(1); -// verify(callFetchSongDerived).called(1); -// verify(callFetchSongRelated).called(1); - -// await r.expectTagsVisible(true); -// await r.expectArtistsVisible(true); - -// await r.scrollDown(); -// await tester.pump(); - -// await r.expectPVsVisible(true); -// await r.expectAlbumsVisible(true); -// await r.expectAltSongsVisible(true); - -// await r.scrollDown(); -// await tester.pump(); - -// await r.expectSameLikeSongsVisible(true); -// await r.expectWebLinksVisible(true); - -// // Verify no extra call -// verifyNever(callFetchSongId); -// verifyNever(callFetchSongDerived); -// verifyNever(callFetchSongRelated); -// }); - -// testWidgets('song detail screen with song detail all fields is default', -// (tester) async { -// /// Setup -// final r = SongRobot(tester); -// final songRepository = MockSongRepository(); -// final authRepository = MockAuthRepository(); -// const songId = 1; - -// /// Mock -// callFetchSongId() => -// songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); -// callFetchSongDerived() => songRepository.fetchSongsDerived(songId, -// lang: PreferredLang.Default.name); -// callFetchSongRelated() => songRepository.fetchSongsRelated(songId, -// lang: PreferredLang.Default.name); - -// when(callFetchSongId) -// .thenAnswer((_) => Future.value(const Song(id: songId))); -// when(callFetchSongDerived).thenAnswer((_) => Future.value([])); -// when(callFetchSongRelated).thenAnswer( -// (_) => Future.value(const SongRelated()), -// ); - -// /// Pump screen -// await r.pumpSongDetailScreen( -// songRepository: songRepository, -// authRepository: authRepository, -// songId: songId, -// ); - -// // Verify -// verify(callFetchSongId).called(1); -// verify(callFetchSongDerived).called(1); -// verify(callFetchSongRelated).called(1); - -// await r.expectLyricButtonVisible(false); -// await r.expectTagsVisible(false); -// await r.expectArtistsVisible(false); - -// await r.scrollDown(); -// await tester.pump(); - -// await r.expectPVsVisible(false); -// await r.expectAlbumsVisible(false); -// await r.expectAltSongsVisible(false); - -// await r.scrollDown(); -// await tester.pump(); - -// await r.expectSameLikeSongsVisible(false); -// await r.expectWebLinksVisible(false); -// }); - -// testWidgets('song detail screen with fetch song detail failure', -// (tester) async { -// final r = SongRobot(tester); -// final songRepository = MockSongRepository(); -// final authRepository = MockAuthRepository(); -// const songId = 1; - -// // Mock -// callFetchSongId() => -// songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); - -// when(callFetchSongId).thenThrow( -// Exception('Connection error'), -// ); - -// await r.pumpSongDetailScreen( -// songRepository: songRepository, -// authRepository: authRepository, -// songId: songId, -// ); - -// verify(callFetchSongId).called(1); -// }); - -// testWidgets('song detail screen toggle lyric', (tester) async { -// /// Setup -// final r = SongRobot(tester); -// final songRepository = MockSongRepository(); -// final authRepository = MockAuthRepository(); -// final song = kFakeSongDetail; -// final songId = song.id; - -// /// Mock -// callFetchSongId() => -// songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); -// callFetchSongDerived() => songRepository.fetchSongsDerived(songId, -// lang: PreferredLang.Default.name); -// callFetchSongRelated() => songRepository.fetchSongsRelated(songId, -// lang: PreferredLang.Default.name); - -// // Mock -// when(callFetchSongId).thenAnswer((_) => Future.value(song)); -// when(callFetchSongDerived).thenAnswer((_) => Future.value([])); -// when(callFetchSongRelated) -// .thenAnswer((_) => Future.value(const SongRelated())); - -// /// Pump screen -// await r.pumpSongDetailScreen( -// songRepository: songRepository, -// authRepository: authRepository, -// songId: song.id, -// ); - -// verify(callFetchSongId).called(1); -// verify(callFetchSongDerived).called(1); -// verify(callFetchSongRelated).called(1); - -// /// Verify before tap lyric button -// await r.expectSongInfoContentVisible(true); -// await r.expectSongLyricContentVisible(false); -// await r.expectLyricButtonVisible(true); - -// /// Verify after tap lyric button -// await r.tapLyricButton(); -// await r.expectSongInfoContentVisible(false); -// await r.expectSongLyricContentVisible(true); - -// /// Verify after tap close lyric button -// await r.tapCloseLyricButton(); -// await r.expectSongInfoContentVisible(true); -// await r.expectSongLyricContentVisible(false); -// }); + + testWidgets('song detail screen with fetch song detail success', + (tester) async { + /// Setup + final r = SongRobot(tester); + final songRepository = MockSongRepository(); + final authRepository = MockAuthRepository(); + final song = kFakeSongDetail; + + /// Mock + callFetchSongId() => + songRepository.fetchSongId(song.id, lang: PreferredLang.Default.name); + callFetchSongDerived() => songRepository.fetchSongsDerived(song.id, + lang: PreferredLang.Default.name); + callFetchSongRelated() => songRepository.fetchSongsRelated(song.id, + lang: PreferredLang.Default.name); + + when(callFetchSongId).thenAnswer((_) => Future.value(song)); + when(callFetchSongDerived).thenAnswer((_) => Future.value([ + const Song(id: 1, name: 'Song_A'), + const Song(id: 2, name: 'Song_B'), + ])); + + when(callFetchSongRelated).thenAnswer( + (_) => Future.value( + const SongRelated(likeMatches: [ + Song(id: 3, name: 'Song_Related_A'), + Song(id: 4, name: 'Song_Related_B'), + ]), + ), + ); + + /// Pump screen + await r.pumpSongDetailScreen( + songRepository: songRepository, + authRepository: authRepository, + songId: song.id, + ); + + // Verify + verify(callFetchSongId).called(1); + verify(callFetchSongDerived).called(1); + verify(callFetchSongRelated).called(1); + + await r.expectTagsVisible(true); + await r.expectArtistsVisible(true); + + await r.scrollDown(); + await tester.pump(); + + await r.expectPVsVisible(true); + await r.expectAlbumsVisible(true); + await r.expectAltSongsVisible(true); + + await r.scrollDown(); + await tester.pump(); + + await r.expectSameLikeSongsVisible(true); + await r.expectWebLinksVisible(true); + + // Verify no extra call + verifyNever(callFetchSongId); + verifyNever(callFetchSongDerived); + verifyNever(callFetchSongRelated); + }); + + testWidgets('song detail screen with song detail all fields is default', + (tester) async { + /// Setup + final r = SongRobot(tester); + final songRepository = MockSongRepository(); + final authRepository = MockAuthRepository(); + const songId = 1; + + /// Mock + callFetchSongId() => + songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); + callFetchSongDerived() => songRepository.fetchSongsDerived(songId, + lang: PreferredLang.Default.name); + callFetchSongRelated() => songRepository.fetchSongsRelated(songId, + lang: PreferredLang.Default.name); + + when(callFetchSongId) + .thenAnswer((_) => Future.value(const Song(id: songId))); + when(callFetchSongDerived).thenAnswer((_) => Future.value([])); + when(callFetchSongRelated).thenAnswer( + (_) => Future.value(const SongRelated()), + ); + + /// Pump screen + await r.pumpSongDetailScreen( + songRepository: songRepository, + authRepository: authRepository, + songId: songId, + ); + + // Verify + verify(callFetchSongId).called(1); + verify(callFetchSongDerived).called(1); + verify(callFetchSongRelated).called(1); + + await r.expectLyricButtonVisible(false); + await r.expectTagsVisible(false); + await r.expectArtistsVisible(false); + + await r.scrollDown(); + await tester.pump(); + + await r.expectPVsVisible(false); + await r.expectAlbumsVisible(false); + await r.expectAltSongsVisible(false); + + await r.scrollDown(); + await tester.pump(); + + await r.expectSameLikeSongsVisible(false); + await r.expectWebLinksVisible(false); + }); + + testWidgets('song detail screen with fetch song detail failure', + (tester) async { + final r = SongRobot(tester); + final songRepository = MockSongRepository(); + final authRepository = MockAuthRepository(); + const songId = 1; + + // Mock + callFetchSongId() => + songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); + + when(callFetchSongId).thenThrow( + Exception('Connection error'), + ); + + await r.pumpSongDetailScreen( + songRepository: songRepository, + authRepository: authRepository, + songId: songId, + ); + + verify(callFetchSongId).called(1); + }); + + testWidgets('song detail screen toggle lyric', (tester) async { + /// Setup + final r = SongRobot(tester); + final songRepository = MockSongRepository(); + final authRepository = MockAuthRepository(); + final song = kFakeSongDetail; + final songId = song.id; + + /// Mock + callFetchSongId() => + songRepository.fetchSongId(songId, lang: PreferredLang.Default.name); + callFetchSongDerived() => songRepository.fetchSongsDerived(songId, + lang: PreferredLang.Default.name); + callFetchSongRelated() => songRepository.fetchSongsRelated(songId, + lang: PreferredLang.Default.name); + + // Mock + when(callFetchSongId).thenAnswer((_) => Future.value(song)); + when(callFetchSongDerived).thenAnswer((_) => Future.value([])); + when(callFetchSongRelated) + .thenAnswer((_) => Future.value(const SongRelated())); + + /// Pump screen + await r.pumpSongDetailScreen( + songRepository: songRepository, + authRepository: authRepository, + songId: song.id, + ); + + verify(callFetchSongId).called(1); + verify(callFetchSongDerived).called(1); + verify(callFetchSongRelated).called(1); + + // /// Verify before tap lyric button + // await r.expectSongInfoContentVisible(true); + await r.expectSongLyricContentVisible(false); + await r.expectLyricButtonVisible(true); + + // /// Verify after tap lyric button + await r.tapLyricButton(); + // await r.expectSongInfoContentVisible(false); + // await r.expectSongLyricContentVisible(true); + + // /// Verify after tap close lyric button + // await r.tapCloseLyricButton(); + // await r.expectSongInfoContentVisible(true); + // await r.expectSongLyricContentVisible(false); + }); } diff --git a/test/src/features/songs/song_robot.dart b/test/src/features/songs/song_robot.dart index 4f9ae6a2..fa7b74fa 100644 --- a/test/src/features/songs/song_robot.dart +++ b/test/src/features/songs/song_robot.dart @@ -38,6 +38,7 @@ class SongRobot { child: MaterialApp( home: SongDetailScreen( song: Song(id: songId), + pvPlayerWidget: const Text('Mock Player Widget'), ), ), ),