diff --git a/app/Services/SmartPlaylistService.php b/app/Services/SmartPlaylistService.php index 40fcb306..3259b54a 100644 --- a/app/Services/SmartPlaylistService.php +++ b/app/Services/SmartPlaylistService.php @@ -13,10 +13,6 @@ use Illuminate\Support\Collection; class SmartPlaylistService { - public function __construct() - { - } - /** @return Collection|array */ public function getSongs(Playlist $playlist, ?User $user = null): Collection { diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 06b4af75..67623b31 100644 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -6,7 +6,6 @@
- @@ -25,11 +24,11 @@ diff --git a/resources/assets/js/components/layout/ModalWrapper.vue b/resources/assets/js/components/layout/ModalWrapper.vue index 98d3a00d..7a1d118a 100644 --- a/resources/assets/js/components/layout/ModalWrapper.vue +++ b/resources/assets/js/components/layout/ModalWrapper.vue @@ -96,12 +96,12 @@ eventBus.on({ form { position: relative; min-width: 460px; - max-width: calc(100% - 24px); + max-width: calc(100vw - 24px); background-color: var(--color-bg-primary); border-radius: 4px; - @media only screen and (max-width: 667px) { - min-width: calc(100% - 24px); + @media screen and (max-width: 667px) { + min-width: calc(100vw - 24px); } > header, > main, > footer { diff --git a/resources/assets/js/components/layout/app-footer/AudioPlayer.vue b/resources/assets/js/components/layout/app-footer/AudioPlayer.vue new file mode 100644 index 00000000..8cd4f285 --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/AudioPlayer.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/resources/assets/js/components/layout/app-footer/FooterExtraControls.spec.ts b/resources/assets/js/components/layout/app-footer/FooterExtraControls.spec.ts index a4b99eb4..8ee21b40 100644 --- a/resources/assets/js/components/layout/app-footer/FooterExtraControls.spec.ts +++ b/resources/assets/js/components/layout/app-footer/FooterExtraControls.spec.ts @@ -1,30 +1,22 @@ import { expect, it } from 'vitest' import factory from '@/__tests__/factory' -import { preferenceStore } from '@/stores' import UnitTestCase from '@/__tests__/UnitTestCase' +import { CurrentSongKey } from '@/symbols' import FooterExtraControls from './FooterExtraControls.vue' new class extends UnitTestCase { protected test () { it('renders', () => { - preferenceStore.state.showExtraPanel = true - expect(this.render(FooterExtraControls, { - props: { - song: factory('song', { - playback_state: 'Playing', - title: 'Fahrstuhl to Heaven', - artist_name: 'Led Zeppelin', - artist_id: 3, - album_name: 'Led Zeppelin IV', - album_id: 4, - liked: false - }) - }, global: { stubs: { - RepeatModeSwitch: this.stub('RepeatModeSwitch'), + Equalizer: this.stub('Equalizer'), Volume: this.stub('Volume') + }, + provide: { + [CurrentSongKey]: factory('song', { + playback_state: 'Playing' + }) } } }).html()).toMatchSnapshot() diff --git a/resources/assets/js/components/layout/app-footer/FooterExtraControls.vue b/resources/assets/js/components/layout/app-footer/FooterExtraControls.vue index 74e5866f..aa8cdf46 100644 --- a/resources/assets/js/components/layout/app-footer/FooterExtraControls.vue +++ b/resources/assets/js/components/layout/app-footer/FooterExtraControls.vue @@ -1,11 +1,11 @@ diff --git a/resources/assets/js/components/layout/app-footer/FooterMiddlePane.spec.ts b/resources/assets/js/components/layout/app-footer/FooterMiddlePane.spec.ts deleted file mode 100644 index 6bfd4bd2..00000000 --- a/resources/assets/js/components/layout/app-footer/FooterMiddlePane.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, it } from 'vitest' -import factory from '@/__tests__/factory' -import UnitTestCase from '@/__tests__/UnitTestCase' -import FooterMiddlePane from './FooterMiddlePane.vue' - -new class extends UnitTestCase { - protected test () { - it('renders without a song', () => expect(this.render(FooterMiddlePane).html()).toMatchSnapshot()) - - it('renders with a song', () => { - expect(this.render(FooterMiddlePane, { - props: { - song: factory('song', { - title: 'Fahrstuhl to Heaven', - artist_name: 'Led Zeppelin', - artist_id: 3, - album_name: 'Led Zeppelin IV', - album_id: 4 - }) - } - }).html()).toMatchSnapshot() - }) - } -} diff --git a/resources/assets/js/components/layout/app-footer/FooterMiddlePane.vue b/resources/assets/js/components/layout/app-footer/FooterMiddlePane.vue deleted file mode 100644 index d4221f83..00000000 --- a/resources/assets/js/components/layout/app-footer/FooterMiddlePane.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.spec.ts b/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.spec.ts new file mode 100644 index 00000000..2f97f352 --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.spec.ts @@ -0,0 +1,58 @@ +import { ref } from 'vue' +import { expect, it } from 'vitest' +import factory from '@/__tests__/factory' +import UnitTestCase from '@/__tests__/UnitTestCase' +import { CurrentSongKey } from '@/symbols' +import { fireEvent } from '@testing-library/vue' +import { playbackService } from '@/services' +import FooterPlaybackControls from './FooterPlaybackControls.vue' + +new class extends UnitTestCase { + private renderComponent (song?: Song | null) { + if (song === undefined) { + song = factory('song', { + id: 42, + title: 'Fahrstuhl to Heaven', + artist_name: 'Led Zeppelin', + artist_id: 3, + album_name: 'Led Zeppelin IV', + album_id: 4, + liked: true + }) + } + + return this.render(FooterPlaybackControls, { + global: { + stubs: { + PlayButton: this.stub('PlayButton') + }, + provide: { + [CurrentSongKey]: ref(song) + } + } + }) + } + + protected test () { + it('renders without a current song', () => expect(this.renderComponent(null).html()).toMatchSnapshot()) + it('renders with a current song', () => expect(this.renderComponent().html()).toMatchSnapshot()) + + it('plays the previous song', async () => { + const playMock = this.mock(playbackService, 'playPrev') + const { getByTitle } = this.renderComponent() + + await fireEvent.click(getByTitle('Play previous song')) + + expect(playMock).toHaveBeenCalled() + }) + + it('plays the next song', async () => { + const playMock = this.mock(playbackService, 'playNext') + const { getByTitle } = this.renderComponent() + + await fireEvent.click(getByTitle('Play next song')) + + expect(playMock).toHaveBeenCalled() + }) + } +} diff --git a/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.vue b/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.vue new file mode 100644 index 00000000..474f3dd6 --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/FooterPlaybackControls.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/resources/assets/js/components/layout/app-footer/FooterPlayerControl.spec.ts b/resources/assets/js/components/layout/app-footer/FooterPlayerControl.spec.ts deleted file mode 100644 index b8557598..00000000 --- a/resources/assets/js/components/layout/app-footer/FooterPlayerControl.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect, it } from 'vitest' -import { fireEvent } from '@testing-library/vue' -import { playbackService } from '@/services' -import factory from '@/__tests__/factory' -import UnitTestCase from '@/__tests__/UnitTestCase' -import FooterPlayerControls from './FooterPlayerControls.vue' - -new class extends UnitTestCase { - protected test () { - it.each<[string, string, MethodOf]>([ - ['plays next song', 'Play next song', 'playNext'], - ['plays previous song', 'Play previous song', 'playPrev'], - ['plays/resumes current song', 'Play or resume', 'toggle'] - ])('%s', async (_: string, title: string, playbackMethod: MethodOf) => { - const mock = this.mock(playbackService, playbackMethod) - - const { getByTitle } = this.render(FooterPlayerControls, { - props: { - song: factory('song') - } - }) - - await fireEvent.click(getByTitle(title)) - expect(mock).toHaveBeenCalled() - }) - - it('pauses the current song', async () => { - const mock = this.mock(playbackService, 'toggle') - - const { getByTitle } = this.render(FooterPlayerControls, { - props: { - song: factory('song', { - playback_state: 'Playing' - }) - } - }) - - await fireEvent.click(getByTitle('Pause')) - expect(mock).toHaveBeenCalled() - }) - } -} diff --git a/resources/assets/js/components/layout/app-footer/FooterPlayerControls.vue b/resources/assets/js/components/layout/app-footer/FooterPlayerControls.vue deleted file mode 100644 index 0044b879..00000000 --- a/resources/assets/js/components/layout/app-footer/FooterPlayerControls.vue +++ /dev/null @@ -1,222 +0,0 @@ - - - - - diff --git a/resources/assets/js/components/layout/app-footer/FooterSongInfo.spec.ts b/resources/assets/js/components/layout/app-footer/FooterSongInfo.spec.ts new file mode 100644 index 00000000..424848eb --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/FooterSongInfo.spec.ts @@ -0,0 +1,30 @@ +import { expect, it } from 'vitest' +import factory from '@/__tests__/factory' +import UnitTestCase from '@/__tests__/UnitTestCase' +import FooterSongInfo from './FooterSongInfo.vue' +import { ref } from 'vue' +import { CurrentSongKey } from '@/symbols' + +new class extends UnitTestCase { + protected test () { + it('renders with no current song', () => expect(this.render(FooterSongInfo).html()).toMatchSnapshot()) + + it('renders with current song', () => { + const song = factory('song', { + title: 'Fahrstuhl zum Mond', + album_cover: 'https://via.placeholder.com/150', + playback_state: 'Playing', + artist_id: 10, + artist_name: 'Led Zeppelin' + }) + + expect(this.render(FooterSongInfo, { + global: { + provide: { + [CurrentSongKey]: ref(song) + } + } + }).html()).toMatchSnapshot() + }) + } +} diff --git a/resources/assets/js/components/layout/app-footer/FooterSongInfo.vue b/resources/assets/js/components/layout/app-footer/FooterSongInfo.vue new file mode 100644 index 00000000..796d8bbe --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/FooterSongInfo.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterExtraControls.spec.ts.snap b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterExtraControls.spec.ts.snap index 4a7bb098..9e3a8048 100644 --- a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterExtraControls.spec.ts.snap +++ b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterExtraControls.spec.ts.snap @@ -1,9 +1,10 @@ // Vitest Snapshot v1 exports[`renders 1`] = ` -
+
-


+ +
`; diff --git a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterMiddlePane.spec.ts.snap b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterMiddlePane.spec.ts.snap deleted file mode 100644 index bc341873..00000000 --- a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterMiddlePane.spec.ts.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Vitest Snapshot v1 - -exports[`renders with a song 1`] = ` -
-
-

Fahrstuhl to Heaven

-

Led ZeppelinLed Zeppelin IV

-
-
-
-`; - -exports[`renders without a song 1`] = ` -
-
- -
-
-
-`; diff --git a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterPlaybackControls.spec.ts.snap b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterPlaybackControls.spec.ts.snap new file mode 100644 index 00000000..9ffb8f64 --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterPlaybackControls.spec.ts.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1 + +exports[`renders with a current song 1`] = ` +
+

+
+`; + +exports[`renders without a current song 1`] = ` +
+

+
+`; diff --git a/resources/assets/js/components/layout/app-footer/__snapshots__/FooterSongInfo.spec.ts.snap b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterSongInfo.spec.ts.snap new file mode 100644 index 00000000..ab22acef --- /dev/null +++ b/resources/assets/js/components/layout/app-footer/__snapshots__/FooterSongInfo.spec.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1 + +exports[`renders with current song 1`] = ` +
+
+

Fahrstuhl zum Mond

Led Zeppelin +
+
+`; + +exports[`renders with no current song 1`] = ` +
+ +
+`; diff --git a/resources/assets/js/components/layout/app-footer/index.vue b/resources/assets/js/components/layout/app-footer/index.vue index 89bd61f5..40f02fed 100644 --- a/resources/assets/js/components/layout/app-footer/index.vue +++ b/resources/assets/js/components/layout/app-footer/index.vue @@ -1,29 +1,30 @@ diff --git a/resources/assets/js/components/layout/main-wrapper/ExtraPanel.spec.ts b/resources/assets/js/components/layout/main-wrapper/ExtraPanel.spec.ts index 3b4c9d82..b2ed7979 100644 --- a/resources/assets/js/components/layout/main-wrapper/ExtraPanel.spec.ts +++ b/resources/assets/js/components/layout/main-wrapper/ExtraPanel.spec.ts @@ -1,44 +1,73 @@ +import { ref, Ref } from 'vue' import { expect, it } from 'vitest' -import { fireEvent } from '@testing-library/vue' +import { fireEvent, waitFor } from '@testing-library/vue' import factory from '@/__tests__/factory' -import { commonStore } from '@/stores' +import { albumStore, artistStore, commonStore } from '@/stores' import UnitTestCase from '@/__tests__/UnitTestCase' +import { CurrentSongKey } from '@/symbols' import ExtraPanel from './ExtraPanel.vue' +import { eventBus } from '@/utils' new class extends UnitTestCase { - private renderComponent () { + private renderComponent (songRef: Ref = ref(null)) { return this.render(ExtraPanel, { - props: { - song: factory('song') - }, global: { stubs: { - LyricsPane: this.stub(), - AlbumInfo: this.stub(), - ArtistInfo: this.stub(), - YouTubeVideoList: this.stub() + ProfileAvatar: this.stub(), + LyricsPane: this.stub('lyrics'), + AlbumInfo: this.stub('album-info'), + ArtistInfo: this.stub('artist-info'), + YouTubeVideoList: this.stub('youtube-video-list'), + ExtraPanelTabHeader: this.stub() + }, + provide: { + [CurrentSongKey]: songRef } } }) } protected test () { - it('has a YouTube tab if using YouTube ', () => { + it('renders without a current song', () => expect(this.renderComponent().html()).toMatchSnapshot()) + + it('fetches info for the current song', async () => { commonStore.state.use_you_tube = true - this.renderComponent().getByTestId('extra-tab-youtube') + const artist = factory('artist') + const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist) + + const album = factory('album') + const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album) + + const song = factory('song') + + const songRef = ref(null) + + const { getByTestId } = this.renderComponent(songRef) + songRef.value = song + + await waitFor(() => { + expect(resolveArtistMock).toHaveBeenCalledWith(song.artist_id) + expect(resolveAlbumMock).toHaveBeenCalledWith(song.album_id) + ;['lyrics', 'album-info', 'artist-info', 'youtube-video-list'].forEach(id => getByTestId(id)) + }) }) - it('does not have a YouTube tab if not using YouTube', () => { - commonStore.state.use_you_tube = false - expect(this.renderComponent().queryByTestId('extra-tab-youtube')).toBeNull() + it('shows About Koel model', async () => { + const emitMock = this.mock(eventBus, 'emit') + const { getByTitle } = this.renderComponent() + + await fireEvent.click(getByTitle('About Koel')) + + expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_ABOUT_KOEL') }) - it.each([['extra-tab-lyrics'], ['extra-tab-album'], ['extra-tab-artist']])('switches to "%s" tab', async (id) => { - const { getByTestId, container } = this.renderComponent() + it('logs out', async () => { + const emitMock = this.mock(eventBus, 'emit') + const { getByTitle } = this.renderComponent() - await fireEvent.click(getByTestId(id)) + await fireEvent.click(getByTitle('Log out')) - expect(container.querySelector('[aria-selected=true]')).toBe(getByTestId(id)) + expect(emitMock).toHaveBeenCalledWith('LOG_OUT') }) } } diff --git a/resources/assets/js/components/layout/main-wrapper/ExtraPanel.vue b/resources/assets/js/components/layout/main-wrapper/ExtraPanel.vue index ce1e5d34..373aaf21 100644 --- a/resources/assets/js/components/layout/main-wrapper/ExtraPanel.vue +++ b/resources/assets/js/components/layout/main-wrapper/ExtraPanel.vue @@ -1,163 +1,143 @@ - diff --git a/resources/assets/js/components/screens/AlbumScreen.vue b/resources/assets/js/components/screens/AlbumScreen.vue index bd1a1318..1ad28838 100644 --- a/resources/assets/js/components/screens/AlbumScreen.vue +++ b/resources/assets/js/components/screens/AlbumScreen.vue @@ -101,7 +101,7 @@ const download = () => downloadService.fromAlbum(album.value!) const showInfo = () => (showingInfo.value = true) onMounted(async () => { - const id = parseInt(router.$currentRoute.value?.params!.id) + const id = parseInt(router.$currentRoute.value.params!.id) loading.value = true try { diff --git a/resources/assets/js/components/screens/ArtistScreen.vue b/resources/assets/js/components/screens/ArtistScreen.vue index c4fa78a6..0372dc18 100644 --- a/resources/assets/js/components/screens/ArtistScreen.vue +++ b/resources/assets/js/components/screens/ArtistScreen.vue @@ -102,7 +102,7 @@ const download = () => downloadService.fromArtist(artist.value!) const showInfo = () => (showingInfo.value = true) onMounted(async () => { - const id = parseInt(router.$currentRoute.value!.params!.id) + const id = parseInt(router.$currentRoute.value.params!.id) loading.value = true try { diff --git a/resources/assets/js/components/screens/HomeScreen.vue b/resources/assets/js/components/screens/HomeScreen.vue index 42456688..f4581285 100644 --- a/resources/assets/js/components/screens/HomeScreen.vue +++ b/resources/assets/js/components/screens/HomeScreen.vue @@ -111,7 +111,7 @@ useScreen('Home').onScreenActivated(async () => { } .main-scroll-wrap { - section { + section:not(:last-of-type) { margin-bottom: 48px; } diff --git a/resources/assets/js/components/screens/YouTubeScreen.vue b/resources/assets/js/components/screens/YouTubeScreen.vue index f998eed9..1d43a48e 100644 --- a/resources/assets/js/components/screens/YouTubeScreen.vue +++ b/resources/assets/js/components/screens/YouTubeScreen.vue @@ -17,10 +17,11 @@ diff --git a/resources/assets/js/components/song/SongCard.vue b/resources/assets/js/components/song/SongCard.vue index 445c4880..b89e40cf 100644 --- a/resources/assets/js/components/song/SongCard.vue +++ b/resources/assets/js/components/song/SongCard.vue @@ -79,6 +79,7 @@ article { } button { + color: var(--color-text-secondary); opacity: 0; } diff --git a/resources/assets/js/components/song/SongLikeButton.vue b/resources/assets/js/components/song/SongLikeButton.vue index 8d848ac7..ed0c7c0f 100644 --- a/resources/assets/js/components/song/SongLikeButton.vue +++ b/resources/assets/js/components/song/SongLikeButton.vue @@ -1,6 +1,6 @@ @@ -18,11 +18,3 @@ const title = computed(() => `${song.value.liked ? 'Unlike' : 'Like'} ${song.val const toggleLike = () => favoriteStore.toggleOne(song.value) - - diff --git a/resources/assets/js/components/song/SongList.vue b/resources/assets/js/components/song/SongList.vue index b295e536..0adff4d2 100644 --- a/resources/assets/js/components/song/SongList.vue +++ b/resources/assets/js/components/song/SongList.vue @@ -308,6 +308,10 @@ onMounted(() => render()) display: flex; flex-direction: column; + @media screen and (max-width: 768px) { + padding: 0 12px; + } + .song-list-header { background: var(--color-bg-secondary); z-index: 1; @@ -402,8 +406,6 @@ onMounted(() => render()) } @media only screen and (max-width: 768px) { - padding: 12px; - .song-list-header { display: none; } diff --git a/resources/assets/js/components/song/SongListItem.vue b/resources/assets/js/components/song/SongListItem.vue index 77f91ee0..1cbe9afb 100644 --- a/resources/assets/js/components/song/SongListItem.vue +++ b/resources/assets/js/components/song/SongListItem.vue @@ -10,10 +10,10 @@ {{ song.track || '' }} - {{ song.title }} + {{ song.title }} {{ song.artist_name }} {{ song.album_name }} - {{ fmtLength }} + {{ fmtLength }} @@ -65,6 +65,7 @@ const doPlayback = () => { diff --git a/resources/assets/js/components/ui/AlbumArtistThumbnail.vue b/resources/assets/js/components/ui/AlbumArtistThumbnail.vue index 77268cb2..e37aef97 100644 --- a/resources/assets/js/components/ui/AlbumArtistThumbnail.vue +++ b/resources/assets/js/components/ui/AlbumArtistThumbnail.vue @@ -171,7 +171,6 @@ const onDrop = async (event: DragEvent) => { justify-content: center; align-items: center; padding-left: 4%; // to balance the play icon - z-index: 99; pointer-events: none; @media (hover: none) { diff --git a/resources/assets/js/components/ui/BtnScrollToTop.vue b/resources/assets/js/components/ui/BtnScrollToTop.vue index d0b0141d..143552db 100644 --- a/resources/assets/js/components/ui/BtnScrollToTop.vue +++ b/resources/assets/js/components/ui/BtnScrollToTop.vue @@ -31,7 +31,7 @@ button { } position: fixed; - bottom: calc(var(--footer-height-mobile) + 26px); + bottom: calc(var(--footer-height) + 26px); right: 1.8rem; z-index: 20; opacity: 1; diff --git a/resources/assets/js/components/ui/ContextMenuBase.vue b/resources/assets/js/components/ui/ContextMenuBase.vue index b6c0334a..4b4920a7 100644 --- a/resources/assets/js/components/ui/ContextMenuBase.vue +++ b/resources/assets/js/components/ui/ContextMenuBase.vue @@ -97,3 +97,9 @@ eventBus.on('CONTEXT_MENU_OPENED', target => target === el || close()) defineExpose({ open, close, shown }) + + diff --git a/resources/assets/js/components/ui/Equalizer.vue b/resources/assets/js/components/ui/Equalizer.vue index 38cd466f..7ff565d3 100644 --- a/resources/assets/js/components/ui/Equalizer.vue +++ b/resources/assets/js/components/ui/Equalizer.vue @@ -185,6 +185,7 @@ onMounted(() => eventBus.on('INIT_EQUALIZER', () => init())) display: flex; flex-direction: column; left: 0; + box-shadow: 0 0 50x 0 var(--color-bg-primary); label { margin-top: 8px; @@ -336,7 +337,7 @@ onMounted(() => eventBus.on('INIT_EQUALIZER', () => init())) max-width: 414px; left: auto; right: 0; - bottom: calc(var(--footer-height-mobile) + 0px); + bottom: var(--footer-height); display: block; height: auto; diff --git a/resources/assets/js/components/ui/ExtraPanelTabHeader.spec.ts b/resources/assets/js/components/ui/ExtraPanelTabHeader.spec.ts new file mode 100644 index 00000000..028ff8e2 --- /dev/null +++ b/resources/assets/js/components/ui/ExtraPanelTabHeader.spec.ts @@ -0,0 +1,32 @@ +import { expect, it } from 'vitest' +import UnitTestCase from '@/__tests__/UnitTestCase' +import ExtraPanelTabHeader from './ExtraPanelTabHeader.vue' +import { commonStore } from '@/stores' +import { fireEvent } from '@testing-library/vue' + +new class extends UnitTestCase { + protected test () { + it('renders tab headers', () => { + commonStore.state.use_you_tube = false + const { getByTitle, queryByTitle } = this.render(ExtraPanelTabHeader) + + ;['Lyrics', 'Artist information', 'Album information'].forEach(title => getByTitle(title)) + expect(queryByTitle('Related YouTube videos')).toBeNull() + }) + + it('has a YouTube tab header if using YouTube', () => { + commonStore.state.use_you_tube = true + const { getByTitle } = this.render(ExtraPanelTabHeader) + + getByTitle('Related YouTube videos') + }) + + it('emits the selected tab value', async () => { + const { getByTitle, emitted } = this.render(ExtraPanelTabHeader) + + await fireEvent.click(getByTitle('Lyrics')) + + expect(emitted()['update:modelValue']).toEqual([['Lyrics']]) + }) + } +} diff --git a/resources/assets/js/components/ui/ExtraPanelTabHeader.vue b/resources/assets/js/components/ui/ExtraPanelTabHeader.vue new file mode 100644 index 00000000..7c72619e --- /dev/null +++ b/resources/assets/js/components/ui/ExtraPanelTabHeader.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/resources/assets/js/components/ui/FooterPlayButton.spec.ts b/resources/assets/js/components/ui/FooterPlayButton.spec.ts new file mode 100644 index 00000000..221a488b --- /dev/null +++ b/resources/assets/js/components/ui/FooterPlayButton.spec.ts @@ -0,0 +1,125 @@ +import factory from '@/__tests__/factory' +import { ref } from 'vue' +import { expect, it } from 'vitest' +import UnitTestCase from '@/__tests__/UnitTestCase' +import { playbackService } from '@/services' +import { fireEvent, getByRole, waitFor } from '@testing-library/vue' +import { CurrentSongKey } from '@/symbols' +import { commonStore, favoriteStore, queueStore, recentlyPlayedStore, songStore } from '@/stores' +import FooterPlayButton from './FooterPlayButton.vue' + +new class extends UnitTestCase { + private renderComponent (currentSong: Song | null = null) { + return this.render(FooterPlayButton, { + global: { + provide: { + [CurrentSongKey]: ref(currentSong) + } + } + }) + } + + protected test () { + it('toggles the playback of current song', async () => { + const toggleMock = this.mock(playbackService, 'toggle') + const { getByRole } = this.renderComponent(factory('song')) + + await fireEvent.click(getByRole('button')) + + expect(toggleMock).toHaveBeenCalled() + }) + + it.each<[ScreenName, MethodOf]>([ + ['Album', 'fetchForAlbum'], + ['Artist', 'fetchForArtist'], + ['Playlist', 'fetchForPlaylist'] + ])('initiates playback for %s screen', async (screen, fetchMethod) => { + commonStore.state.song_count = 10 + const songs = factory('song', 3) + const fetchMock = this.mock(songStore, fetchMethod).mockResolvedValue(songs) + const playMock = this.mock(playbackService, 'queueAndPlay') + const goMock = this.mock(this.router, 'go') + + this.router.activateRoute({ + screen, + path: '_' + }, { id: '42' }) + + const { getByRole } = this.renderComponent() + + await fireEvent.click(getByRole('button')) + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(42) + expect(playMock).toHaveBeenCalledWith(songs) + expect(goMock).toHaveBeenCalledWith('queue') + }) + }) + + it.each<[ScreenName, object, string]>([ + ['Favorites', favoriteStore, 'fetch'], + ['RecentlyPlayed', recentlyPlayedStore, 'fetch'] + ])('initiates playback for %s screen', async (screen, store, fetchMethod) => { + commonStore.state.song_count = 10 + const songs = factory('song', 3) + const fetchMock = this.mock(store, fetchMethod).mockResolvedValue(songs) + const playMock = this.mock(playbackService, 'queueAndPlay') + const goMock = this.mock(this.router, 'go') + + this.router.activateRoute({ + screen, + path: '_' + }) + + const { getByRole } = this.renderComponent() + + await fireEvent.click(getByRole('button')) + await waitFor(() => { + expect(fetchMock).toHaveBeenCalled() + expect(playMock).toHaveBeenCalledWith(songs) + expect(goMock).toHaveBeenCalledWith('queue') + }) + }) + + it.each<[ScreenName]>([['Queue'], ['Songs'], ['Albums']])('initiates playback %s screen', async (screen) => { + commonStore.state.song_count = 10 + const songs = factory('song', 3) + const fetchMock = this.mock(queueStore, 'fetchRandom').mockResolvedValue(songs) + const playMock = this.mock(playbackService, 'queueAndPlay') + const goMock = this.mock(this.router, 'go') + + this.router.activateRoute({ + screen, + path: '_' + }) + + const { getByRole } = this.renderComponent() + + await fireEvent.click(getByRole('button')) + await waitFor(() => { + expect(fetchMock).toHaveBeenCalled() + expect(playMock).toHaveBeenCalledWith(songs) + expect(goMock).toHaveBeenCalledWith('queue') + }) + }) + + it('does nothing if there are no songs', async () => { + commonStore.state.song_count = 0 + + const playMock = this.mock(playbackService, 'queueAndPlay') + const goMock = this.mock(this.router, 'go') + + this.router.activateRoute({ + screen: 'Songs', + path: '_' + }) + + const { getByRole } = this.renderComponent() + + await fireEvent.click(getByRole('button')) + await waitFor(() => { + expect(playMock).not.toHaveBeenCalled() + expect(goMock).not.toHaveBeenCalled() + }) + }) + } +} diff --git a/resources/assets/js/components/ui/FooterPlayButton.vue b/resources/assets/js/components/ui/FooterPlayButton.vue new file mode 100644 index 00000000..5fae3c80 --- /dev/null +++ b/resources/assets/js/components/ui/FooterPlayButton.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/resources/assets/js/components/ui/ProfileAvatar.spec.ts b/resources/assets/js/components/ui/ProfileAvatar.spec.ts new file mode 100644 index 00000000..731b74de --- /dev/null +++ b/resources/assets/js/components/ui/ProfileAvatar.spec.ts @@ -0,0 +1,17 @@ +import { expect, it } from 'vitest' +import factory from '@/__tests__/factory' +import UnitTestCase from '@/__tests__/UnitTestCase' +import ProfileAvatar from './ProfileAvatar.vue' + +new class extends UnitTestCase { + protected test () { + it('renders', () => { + const user = factory('user', { + name: 'John Doe', + avatar: 'https://example.com/avatar.jpg' + }) + + expect(this.actingAs(user).render(ProfileAvatar).html()).toMatchSnapshot() + }) + } +} diff --git a/resources/assets/js/components/ui/ProfileAvatar.vue b/resources/assets/js/components/ui/ProfileAvatar.vue new file mode 100644 index 00000000..9bcb07ef --- /dev/null +++ b/resources/assets/js/components/ui/ProfileAvatar.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/resources/assets/js/components/ui/RepeatModeSwitch.vue b/resources/assets/js/components/ui/RepeatModeSwitch.vue index 72284dbb..9edfb672 100644 --- a/resources/assets/js/components/ui/RepeatModeSwitch.vue +++ b/resources/assets/js/components/ui/RepeatModeSwitch.vue @@ -2,7 +2,6 @@