refactor: use provide/inject and composable for screen logics

This commit is contained in:
Phan An 2022-09-12 22:33:41 +07:00
parent aea0fabe73
commit e89d0f93ca
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
58 changed files with 223 additions and 175 deletions

View file

@ -32,7 +32,7 @@ new class extends UnitTestCase {
})
it('plays all', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
@ -45,7 +45,7 @@ new class extends UnitTestCase {
})
it('shuffles all', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')

View file

@ -67,7 +67,7 @@ new class extends UnitTestCase {
})
it('plays', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
const { getByTitle } = await this.renderComponent()

View file

@ -8,12 +8,12 @@ new class extends UnitTestCase {
protected test () {
it('displays the tracks', async () => {
const album = factory<Album>('album')
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(factory<Song[]>('song', 5))
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(factory<Song>('song', 5))
const { queryAllByTestId } = this.render(AlbumTrackList, {
props: {
album,
tracks: factory<AlbumTrack[]>('album-track', 3)
tracks: factory<AlbumTrack>('album-track', 3)
}
})

View file

@ -10,7 +10,7 @@ import AlbumTrackListItem from './AlbumTrackListItem.vue'
new class extends UnitTestCase {
private renderComponent (matchedSong?: Song) {
const songsToMatchAgainst = factory<Song[]>('song', 10)
const songsToMatchAgainst = factory<Song>('song', 10)
const album = factory<Album>('album')
const track = factory<AlbumTrack>('album-track', {

View file

@ -60,7 +60,7 @@ new class extends UnitTestCase {
})
it('shuffles', async () => {
const songs = factory<Song[]>('song', 16)
const songs = factory<Song>('song', 16)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')

View file

@ -32,7 +32,7 @@ new class extends UnitTestCase {
})
it('plays all', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
@ -45,7 +45,7 @@ new class extends UnitTestCase {
})
it('shuffles all', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')

View file

@ -61,7 +61,7 @@ new class extends UnitTestCase {
})
it('plays', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
const { getByTitle } = await this.renderComponent()

View file

@ -4,13 +4,13 @@ import factory from '@/__tests__/factory'
import { eventBus } from '@/utils'
import { albumStore, preferenceStore } from '@/stores'
import UnitTestCase from '@/__tests__/UnitTestCase'
import MainContent from '@/components/layout/main-wrapper/MainContent.vue'
import AlbumArtOverlay from '@/components/ui/AlbumArtOverlay.vue'
import MainContent from './MainContent.vue'
new class extends UnitTestCase {
protected test () {
it('has a translucent overlay per album', async () => {
this.mock(albumStore, 'fetchThumbnail').mockResolvedValue('https://foo/bar.jpg')
this.mock(albumStore, 'fetchThumbnail').mockResolvedValue('http://localhost/foo.jpg')
const { getByTestId } = this.render(MainContent, {
global: {

View file

@ -1,9 +1,10 @@
import { ref } from 'vue'
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { albumStore, preferenceStore } from '@/stores'
import { eventBus } from '@/utils'
import { fireEvent, waitFor } from '@testing-library/vue'
import { ActiveScreenKey } from '@/symbols'
import AlbumListScreen from './AlbumListScreen.vue'
new class extends UnitTestCase {
@ -12,8 +13,14 @@ new class extends UnitTestCase {
}
private renderComponent () {
albumStore.state.albums = factory<Album[]>('album', 9)
return this.render(AlbumListScreen)
albumStore.state.albums = factory<Album>('album', 9)
return this.render(AlbumListScreen, {
global: {
provide: {
[<symbol>ActiveScreenKey]: ref('Albums')
}
}
})
}
protected test () {
@ -25,7 +32,6 @@ new class extends UnitTestCase {
preferenceStore.albumsViewMode = mode
const { getByTestId } = this.renderComponent()
eventBus.emit('ACTIVATE_SCREEN', 'Albums')
await waitFor(() => expect(getByTestId('album-list').classList.contains(`as-${mode}`)).toBe(true))
})

View file

@ -27,9 +27,8 @@
<script lang="ts" setup>
import { computed, ref, toRef, watch } from 'vue'
import { eventBus } from '@/utils'
import { albumStore, preferenceStore as preferences } from '@/stores'
import { useInfiniteScroll } from '@/composables'
import { useInfiniteScroll, useScreen } from '@/composables'
import AlbumCard from '@/components/album/AlbumCard.vue'
import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
@ -64,11 +63,11 @@ const fetchAlbums = async () => {
loading.value = false
}
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
if (screen === 'Albums' && !initialized) {
useScreen('Albums').onScreenActivated(async () => {
if (!initialized) {
viewMode.value = preferences.albumsViewMode || 'thumbnails'
await makeScrollable()
initialized = true
await makeScrollable()
}
})
</script>

View file

@ -26,7 +26,7 @@ new class extends UnitTestCase {
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
const songs = factory<Song[]>('song', 13)
const songs = factory<Song>('song', 13)
const fetchSongsMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const rendered = this.render(AlbumScreen, {

View file

@ -1,11 +1,12 @@
import { ref } from 'vue'
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { commonStore, queueStore, songStore } from '@/stores'
import { fireEvent, waitFor } from '@testing-library/vue'
import { eventBus } from '@/utils'
import { playbackService } from '@/services'
import router from '@/router'
import { ActiveScreenKey } from '@/symbols'
import AllSongsScreen from './AllSongsScreen.vue'
new class extends UnitTestCase {
@ -19,12 +20,13 @@ new class extends UnitTestCase {
global: {
stubs: {
SongList: this.stub('song-list')
},
provide: {
[<symbol>ActiveScreenKey]: ref('Songs')
}
}
})
eventBus.emit('ACTIVATE_SCREEN', 'Songs')
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('title', 'asc', 1))
return rendered
}

View file

@ -36,10 +36,10 @@
<script lang="ts" setup>
import { computed, ref, toRef } from 'vue'
import { eventBus, pluralize, secondsToHis } from '@/utils'
import { pluralize, secondsToHis } from '@/utils'
import { commonStore, queueStore, songStore } from '@/stores'
import { playbackService } from '@/services'
import { useSongList } from '@/composables'
import { useScreen, useSongList } from '@/composables'
import router from '@/router'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
@ -102,10 +102,10 @@ const playAll = async (shuffle: boolean) => {
await router.go('queue')
}
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
if (screen === 'Songs' && !initialized) {
await fetchSongs()
useScreen('Songs').onScreenActivated(async () => {
if (!initialized) {
initialized = true
await fetchSongs()
}
})
</script>

View file

@ -1,9 +1,10 @@
import { ref } from 'vue'
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { artistStore, preferenceStore } from '@/stores'
import { eventBus } from '@/utils'
import { fireEvent, waitFor } from '@testing-library/vue'
import { ActiveScreenKey } from '@/symbols'
import ArtistListScreen from './ArtistListScreen.vue'
new class extends UnitTestCase {
@ -12,8 +13,14 @@ new class extends UnitTestCase {
}
private renderComponent () {
artistStore.state.artists = factory<Artist[]>('artist', 9)
return this.render(ArtistListScreen)
artistStore.state.artists = factory<Artist>('artist', 9)
return this.render(ArtistListScreen, {
global: {
provide: {
[<symbol>ActiveScreenKey]: ref('Artists')
}
}
})
}
protected test () {
@ -21,11 +28,10 @@ new class extends UnitTestCase {
expect(this.renderComponent().getAllByTestId('artist-card')).toHaveLength(9)
})
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout from preferences', async (mode) => {
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout:%s from preferences', async (mode) => {
preferenceStore.artistsViewMode = mode
const { getByTestId } = this.renderComponent()
eventBus.emit('ACTIVATE_SCREEN', 'Artists')
const { getByTestId, html } = this.renderComponent()
await waitFor(() => expect(getByTestId('artist-list').classList.contains(`as-${mode}`)).toBe(true))
})

View file

@ -27,9 +27,8 @@
<script lang="ts" setup>
import { computed, ref, toRef, watch } from 'vue'
import { eventBus } from '@/utils'
import { artistStore, preferenceStore as preferences } from '@/stores'
import { useInfiniteScroll } from '@/composables'
import { useInfiniteScroll, useScreen } from '@/composables'
import ArtistCard from '@/components/artist/ArtistCard.vue'
import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
@ -64,11 +63,11 @@ const fetchArtists = async () => {
loading.value = false
}
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
if (screen === 'Artists' && !initialized) {
useScreen('Artists').onScreenActivated(async () => {
if (!initialized) {
viewMode.value = preferences.artistsViewMode || 'thumbnails'
await makeScrollable()
initialized = true
await makeScrollable()
}
})
</script>

View file

@ -25,7 +25,7 @@ new class extends UnitTestCase {
const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist)
const songs = factory<Song[]>('song', 13)
const songs = factory<Song>('song', 13)
const fetchSongsMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const rendered = this.render(ArtistScreen, {

View file

@ -1,17 +1,23 @@
import { ref } from 'vue'
import { waitFor } from '@testing-library/vue'
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { favoriteStore } from '@/stores'
import { ActiveScreenKey } from '@/symbols'
import FavoritesScreen from './FavoritesScreen.vue'
import { eventBus } from '@/utils'
new class extends UnitTestCase {
private async renderComponent () {
const fetchMock = this.mock(favoriteStore, 'fetch')
const rendered = this.render(FavoritesScreen)
const rendered = this.render(FavoritesScreen, {
global: {
provide: {
[<symbol>ActiveScreenKey]: ref('Favorites')
}
}
})
eventBus.emit('ACTIVATE_SCREEN', 'Favorites')
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
return rendered

View file

@ -60,10 +60,10 @@
<script lang="ts" setup>
import { faHeartBroken } from '@fortawesome/free-solid-svg-icons'
import { faHeart } from '@fortawesome/free-regular-svg-icons'
import { eventBus, pluralize } from '@/utils'
import { pluralize } from '@/utils'
import { commonStore, favoriteStore } from '@/stores'
import { downloadService } from '@/services'
import { useSongList } from '@/composables'
import { useScreen, useSongList } from '@/composables'
import { nextTick, ref, toRef } from 'vue'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
@ -106,10 +106,10 @@ const fetchSongs = async () => {
sort()
}
eventBus.on('ACTIVATE_SCREEN', async (screen: ScreenName) => {
if (screen === 'Favorites' && !initialized) {
await fetchSongs()
useScreen('Favorites').onScreenActivated(async () => {
if (!initialized) {
initialized = true
await fetchSongs()
}
})
</script>

View file

@ -1,18 +1,31 @@
import { ref } from 'vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import HomeScreen from './HomeScreen.vue'
import { commonStore } from '@/stores'
import { ActiveScreenKey } from '@/symbols'
import HomeScreen from './HomeScreen.vue'
new class extends UnitTestCase {
private renderComponent () {
return this.render(HomeScreen, {
global: {
provide: {
[<symbol>ActiveScreenKey]: ref('Home')
}
}
})
}
protected test () {
it('renders an empty state if no songs found', () => {
commonStore.state.song_length = 0
this.render(HomeScreen).getByTestId('screen-empty-state')
this.renderComponent().getByTestId('screen-empty-state')
})
it('renders overview components if applicable', () => {
commonStore.state.song_length = 100
const { getByTestId, queryByTestId } = this.render(HomeScreen)
const { getByTestId, queryByTestId } = this.renderComponent()
;[
'most-played-songs',
@ -21,7 +34,7 @@ new class extends UnitTestCase {
'recently-added-songs',
'most-played-artists',
'most-played-albums'
].forEach(getByTestId)
].forEach(id => getByTestId(id))
expect(queryByTestId('screen-empty-state')).toBeNull()
})

View file

@ -37,9 +37,9 @@
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
import { sample } from 'lodash'
import { computed, ref } from 'vue'
import { eventBus, noop } from '@/utils'
import { noop } from '@/utils'
import { commonStore, overviewStore, userStore } from '@/stores'
import { useAuthorization, useInfiniteScroll } from '@/composables'
import { useAuthorization, useInfiniteScroll, useScreen } from '@/composables'
import MostPlayedSongs from '@/components/screens/home/MostPlayedSongs.vue'
import RecentlyPlayedSongs from '@/components/screens/home/RecentlyPlayedSongs.vue'
@ -72,8 +72,8 @@ const libraryEmpty = computed(() => commonStore.state.song_length === 0)
const loading = ref(false)
let initialized = false
eventBus.on('ACTIVATE_SCREEN', async (view: ScreenName) => {
if (view === 'Home' && !initialized) {
useScreen('Home').onScreenActivated(async () => {
if (!initialized) {
loading.value = true
await overviewStore.init()
initialized = true

View file

@ -24,7 +24,7 @@ new class extends UnitTestCase {
protected test () {
it('renders the playlist', async () => {
const { getByTestId, queryByTestId } = (await this.renderComponent(factory<Song[]>('song', 10))).rendered
const { getByTestId, queryByTestId } = (await this.renderComponent(factory<Song>('song', 10))).rendered
await waitFor(() => {
getByTestId('song-list')
@ -43,7 +43,7 @@ new class extends UnitTestCase {
it('downloads the playlist', async () => {
const downloadMock = this.mock(downloadService, 'fromPlaylist')
const { getByText } = (await this.renderComponent(factory<Song[]>('song', 10))).rendered
const { getByText } = (await this.renderComponent(factory<Song>('song', 10))).rendered
await this.tick()
await fireEvent.click(getByText('Download All'))

View file

@ -21,7 +21,7 @@ new class extends UnitTestCase {
protected test () {
it('renders the queue', () => {
const { queryByTestId } = this.renderComponent(factory<Song[]>('song', 3))
const { queryByTestId } = this.renderComponent(factory<Song>('song', 3))
expect(queryByTestId('song-list')).toBeTruthy()
expect(queryByTestId('screen-empty-state')).toBeNull()
@ -49,7 +49,7 @@ new class extends UnitTestCase {
})
it('Shuffles all', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const { getByTitle } = this.renderComponent(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')

View file

@ -50,7 +50,7 @@
<script lang="ts" setup>
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, toRefs } from 'vue'
import { computed, ref, toRef } from 'vue'
import { eventBus, logger, pluralize, requireInjection } from '@/utils'
import { commonStore, queueStore, songStore } from '@/stores'
import { playbackService } from '@/services'
@ -64,9 +64,6 @@ import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
const dialog = requireInjection(DialogBoxKey)
const controlConfig: Partial<SongListControlsConfig> = { clearQueue: true }
const props = defineProps<{ song?: string }>()
const { song: queuedSongId } = toRefs(props)
const {
SongList,
SongListControls,
@ -108,7 +105,7 @@ const onPressEnter = () => selectedSongs.value.length && playbackService.play(se
const onReorder = (target: Song) => queueStore.move(selectedSongs.value, target)
eventBus.on('SONG_QUEUED_FROM_ROUTE', async (id: string) => {
let song: Song
let song: Song | undefined
try {
loading.value = true

View file

@ -1,9 +1,10 @@
import { ref } from 'vue'
import { expect, it } from 'vitest'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { recentlyPlayedStore } from '@/stores'
import { eventBus } from '@/utils'
import { waitFor } from '@testing-library/vue'
import { ActiveScreenKey } from '@/symbols'
import RecentlyPlayedScreen from './RecentlyPlayedScreen.vue'
new class extends UnitTestCase {
@ -15,12 +16,13 @@ new class extends UnitTestCase {
global: {
stubs: {
SongList: this.stub('song-list')
},
provide: {
[<symbol>ActiveScreenKey]: ref('RecentlyPlayed')
}
}
})
eventBus.emit('ACTIVATE_SCREEN', 'RecentlyPlayed')
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
return rendered
@ -28,7 +30,7 @@ new class extends UnitTestCase {
protected test () {
it('displays the songs', async () => {
const { queryByTestId } = await this.renderComponent(factory<Song[]>('song', 3))
const { queryByTestId } = await this.renderComponent(factory<Song>('song', 3))
expect(queryByTestId('song-list')).toBeTruthy()
expect(queryByTestId('screen-empty-state')).toBeNull()

View file

@ -37,9 +37,9 @@
<script lang="ts" setup>
import { faClock } from '@fortawesome/free-regular-svg-icons'
import { eventBus, pluralize } from '@/utils'
import { pluralize } from '@/utils'
import { recentlyPlayedStore } from '@/stores'
import { useSongList } from '@/composables'
import { useScreen, useSongList } from '@/composables'
import { ref, toRef } from 'vue'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
@ -69,14 +69,12 @@ const {
let initialized = false
let loading = ref(false)
eventBus.on({
ACTIVATE_SCREEN: async (screen: ScreenName) => {
if (screen === 'RecentlyPlayed' && !initialized) {
loading.value = true
await recentlyPlayedStore.fetch()
loading.value = false
initialized = true
}
useScreen('RecentlyPlayed').onScreenActivated(async () => {
if (!initialized) {
loading.value = true
initialized = true
await recentlyPlayedStore.fetch()
loading.value = false
}
})
</script>

View file

@ -7,7 +7,7 @@ import MostPlayedAlbums from './MostPlayedAlbums.vue'
new class extends UnitTestCase {
protected test () {
it('displays the albums', () => {
overviewStore.state.mostPlayedAlbums = factory<Album[]>('album', 6)
overviewStore.state.mostPlayedAlbums = factory<Album>('album', 6)
expect(this.render(MostPlayedAlbums).getAllByTestId('album-card')).toHaveLength(6)
})
}

View file

@ -7,7 +7,7 @@ import MostPlayedArtists from './MostPlayedArtists.vue'
new class extends UnitTestCase {
protected test () {
it('displays the artists', () => {
overviewStore.state.mostPlayedArtists = factory<Artist[]>('artist', 6)
overviewStore.state.mostPlayedArtists = factory<Artist>('artist', 6)
expect(this.render(MostPlayedArtists).getAllByTestId('artist-card')).toHaveLength(6)
})
}

View file

@ -7,7 +7,7 @@ import MostPlayedSongs from './MostPlayedSongs.vue'
new class extends UnitTestCase {
protected test () {
it('displays the songs', () => {
overviewStore.state.mostPlayedSongs = factory<Song[]>('song', 6)
overviewStore.state.mostPlayedSongs = factory<Song>('song', 6)
expect(this.render(MostPlayedSongs).getAllByTestId('song-card')).toHaveLength(6)
})
}

View file

@ -7,7 +7,7 @@ import RecentlyAddedAlbums from './RecentlyAddedAlbums.vue'
new class extends UnitTestCase {
protected test () {
it('displays the albums', () => {
overviewStore.state.recentlyAddedAlbums = factory<Album[]>('album', 6)
overviewStore.state.recentlyAddedAlbums = factory<Album>('album', 6)
expect(this.render(RecentlyAddedAlbums).getAllByTestId('album-card')).toHaveLength(6)
})
}

View file

@ -7,7 +7,7 @@ import RecentlyAddedSongs from './RecentlyAddedSongs.vue'
new class extends UnitTestCase {
protected test () {
it('displays the songs', () => {
overviewStore.state.recentlyAddedSongs = factory<Song[]>('song', 6)
overviewStore.state.recentlyAddedSongs = factory<Song>('song', 6)
expect(this.render(RecentlyAddedSongs).getAllByTestId('song-card')).toHaveLength(6)
})
}

View file

@ -9,7 +9,7 @@ import router from '@/router'
new class extends UnitTestCase {
protected test () {
it('displays the songs', () => {
recentlyPlayedStore.excerptState.songs = factory<Song[]>('song', 6)
recentlyPlayedStore.excerptState.songs = factory<Song>('song', 6)
expect(this.render(RecentlyPlayedSongs).getAllByTestId('song-card')).toHaveLength(6)
})

View file

@ -62,7 +62,8 @@ new class extends UnitTestCase {
['to bottom', 'queue-bottom', 'queue']
])('queues songs %s', async (_: string, testId: string, queueMethod: MethodOf<typeof queueStore>) => {
queueStore.state.songs = factory<Song>('song', 5)
queueStore.state.current = queueStore.state.songs[1]
queueStore.state.songs[2].playback_state = 'Playing'
const mock = this.mock(queueStore, queueMethod)
const { getByTestId } = this.renderComponent()

View file

@ -90,7 +90,7 @@ const { songs, showing, config } = toRefs(props)
const newPlaylistName = ref('')
const queue = toRef(queueStore.state, 'songs')
const currentSong = toRef(queueStore.state, 'current')
const currentSong = queueStore.current
const allPlaylists = toRef(playlistStore.state, 'playlists')
const playlists = computed(() => allPlaylists.value.filter(playlist => !playlist.is_smart))

View file

@ -38,7 +38,7 @@ new class extends UnitTestCase {
title: 'Rocket to Heaven',
artist_name: 'Led Zeppelin',
album_name: 'IV',
album_cover: 'https://example.co/album.jpg'
album_cover: 'http://localhost/album.jpg'
}))
expect(html()).toMatchSnapshot()

View file

@ -72,6 +72,7 @@ const currentSong = queueStore.current
const onlyOneSongSelected = computed(() => songs.value.length === 1)
const firstSongPlaying = computed(() => songs.value.length ? songs.value[0].playback_state === 'Playing' : false)
const normalPlaylists = computed(() => playlists.value.filter(playlist => !playlist.is_smart))
const { isAdmin } = useAuthorization()
const doPlayback = () => trigger(() => {

View file

@ -9,7 +9,7 @@ import SongListControls from './SongListControls.vue'
new class extends UnitTestCase {
private renderComponent (selectedSongCount = 1, config: Partial<SongListControlsConfig> = {}) {
const songs = factory<Song[]>('song', 5)
const songs = factory<Song>('song', 5)
return this.render(SongListControls, {
props: {

View file

@ -3,7 +3,7 @@
exports[`edits a single song 1`] = `
<div class="edit-song" data-testid="edit-song-form" tabindex="0" data-v-210b4214="">
<form data-v-210b4214="">
<header data-v-210b4214=""><span class="cover" style="background-image: url(https://example.co/album.jpg);" data-v-210b4214=""></span>
<header data-v-210b4214=""><span class="cover" style="background-image: url(http://localhost/album.jpg);" data-v-210b4214=""></span>
<div class="meta" data-v-210b4214="">
<h1 class="" data-v-210b4214="">Rocket to Heaven</h1>
<h2 data-testid="displayed-artist-name" class="" data-v-210b4214="">Led Zeppelin</h2>

View file

@ -23,7 +23,7 @@ new class extends UnitTestCase {
protected test () {
it('fetches and displays the album thumbnail', async () => {
const fetchMock = this.mock(albumStore, 'fetchThumbnail').mockResolvedValue('https://localhost/thumb.jpg')
const fetchMock = this.mock(albumStore, 'fetchThumbnail').mockResolvedValue('http://localhost/thumb.jpg')
const { html } = await this.renderComponent()

View file

@ -47,7 +47,7 @@ new class extends UnitTestCase {
})
it('plays album', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
const { getByRole } = this.renderForAlbum()
@ -61,7 +61,7 @@ new class extends UnitTestCase {
})
it('queues album', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
const queueMock = this.mock(queueStore, 'queue')
const { getByRole } = this.renderForAlbum()
@ -75,7 +75,7 @@ new class extends UnitTestCase {
})
it('plays artist', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
const { getByRole } = this.renderForArtist()
@ -89,7 +89,7 @@ new class extends UnitTestCase {
})
it('queues artist', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const queueMock = this.mock(queueStore, 'queue')
const { getByRole } = this.renderForArtist()

View file

@ -14,10 +14,10 @@ new class extends UnitTestCase {
const searchMock = this.mock(youTubeService, 'searchVideosBySong').mockResolvedValueOnce({
nextPageToken: 'foo',
items: factory<YouTubeVideo[]>('video', 5)
items: factory<YouTubeVideo>('video', 5)
}).mockResolvedValueOnce({
nextPageToken: 'bar',
items: factory<YouTubeVideo[]>('video', 3)
items: factory<YouTubeVideo>('video', 3)
})
const { getAllByTestId, getByRole } = this.render(YouTubeVideoList, {

View file

@ -3,3 +3,5 @@
exports[`displays nothing if fetching fails 1`] = `<div style="background-image: none;" data-testid="album-art-overlay" data-v-75d06710=""></div>`;
exports[`fetches and displays the album thumbnail 1`] = `<div style="background-image: url(https://localhost/thumb.jpg);" data-testid="album-art-overlay" data-v-75d06710=""></div>`;
exports[`fetches and displays the album thumbnail 2`] = `<div style="background-image: url(http://localhost/thumb.jpg);" data-testid="album-art-overlay" data-v-75d06710=""></div>`;

View file

@ -7,3 +7,4 @@ export * from './useNewVersionNotification'
export * from './useThirdPartyServices'
export * from './useDragAndDrop'
export * from './useUpload'
export * from './useScreen'

View file

@ -5,5 +5,8 @@ export const useAuthorization = () => {
const currentUser = toRef(userStore.state, 'current')
const isAdmin = computed(() => currentUser.value?.is_admin)
return { currentUser, isAdmin }
return {
currentUser,
isAdmin
}
}

View file

@ -0,0 +1,16 @@
import { ref, watch } from 'vue'
import { requireInjection } from '@/utils'
import { ActiveScreenKey } from '@/symbols'
export const useScreen = (currentScreen: ScreenName) => {
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
const onScreenActivated = (cb: Closure) => watch(activeScreen, screen => screen === currentScreen && cb(), {
immediate: true
})
return {
activeScreen,
onScreenActivated
}
}

View file

@ -20,11 +20,7 @@ import SongListControls from '@/components/song/SongListControls.vue'
import ThumbnailStack from '@/components/ui/ThumbnailStack.vue'
import { provideReadonly } from '@/utils'
export const useSongList = (
songs: Ref<Song[]>,
type: SongListType,
config: Partial<SongListConfig> = {}
) => {
export const useSongList = (songs: Ref<Song[]>, type: SongListType, config: Partial<SongListConfig> = {}) => {
const songList = ref<InstanceType<typeof SongList>>()
const isPhone = isMobile.phone

View file

@ -1,5 +1,5 @@
import isMobile from 'ismobilejs'
import { computed, toRef } from 'vue'
import { computed, ref, toRef } from 'vue'
import { useAuthorization } from '@/composables/useAuthorization'
import { settingStore } from '@/stores'
import { acceptedMediaTypes, UploadFile } from '@/config'
@ -11,7 +11,7 @@ import router from '@/router'
export const useUpload = () => {
const { isAdmin } = useAuthorization()
const activeScreen = requireInjection(ActiveScreenKey)
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
const toaster = requireInjection(MessageToasterKey)
const mediaPath = toRef(settingStore.state, 'media_path')

View file

@ -20,7 +20,7 @@ new class extends UnitTestCase {
})
it('removes albums by IDs', () => {
const albums = factory<Album[]>('album', 3)
const albums = factory<Album>('album', 3)
albums.forEach(album => albumStore.vault.set(album.id, album))
albumStore.state.albums = albums
@ -50,32 +50,32 @@ new class extends UnitTestCase {
albumStore.syncWithVault(album)
expect(albumStore.vault.size).toBe(1)
expect(albumStore.vault.get(album.id).name).toBe('V')
expect(albumStore.vault.get(album.id)?.name).toBe('V')
})
it('uploads a cover for an album', async () => {
const album = factory<Album>('album')
albumStore.syncWithVault(album)
const songsInAlbum = factory<Song[]>('song', 3, { album_id: album.id })
const putMock = this.mock(httpService, 'put').mockResolvedValue({ coverUrl: 'https://foo/cover.jpg' })
const songsInAlbum = factory<Song>('song', 3, { album_id: album.id })
const putMock = this.mock(httpService, 'put').mockResolvedValue({ coverUrl: 'http://localhost/cover.jpg' })
this.mock(songStore, 'byAlbum', songsInAlbum)
await albumStore.uploadCover(album, 'data://cover')
expect(album.cover).toBe('https://foo/cover.jpg')
expect(album.cover).toBe('http://localhost/cover.jpg')
expect(putMock).toHaveBeenCalledWith(`album/${album.id}/cover`, { cover: 'data://cover' })
expect(albumStore.byId(album.id).cover).toBe('https://foo/cover.jpg')
songsInAlbum.forEach(song => expect(song.album_cover).toBe('https://foo/cover.jpg'))
expect(albumStore.byId(album.id)?.cover).toBe('http://localhost/cover.jpg')
songsInAlbum.forEach(song => expect(song.album_cover).toBe('http://localhost/cover.jpg'))
})
it('fetches an album thumbnail', async () => {
const getMock = this.mock(httpService, 'get').mockResolvedValue({ thumbnailUrl: 'https://foo/thumbnail.jpg' })
const getMock = this.mock(httpService, 'get').mockResolvedValue({ thumbnailUrl: 'http://localhost/thumbnail.jpg' })
const album = factory<Album>('album')
const url = await albumStore.fetchThumbnail(album.id)
expect(getMock).toHaveBeenCalledWith(`album/${album.id}/thumbnail`)
expect(url).toBe('https://foo/thumbnail.jpg')
expect(url).toBe('http://localhost/thumbnail.jpg')
})
it('resolves an album', async () => {
@ -91,7 +91,7 @@ new class extends UnitTestCase {
})
it('paginates', async () => {
const albums = factory<Album[]>('album', 3)
const albums = factory<Album>('album', 3)
this.mock(httpService, 'get').mockResolvedValueOnce({
data: albums,

View file

@ -10,7 +10,7 @@ export const albumStore = {
vault: new Map<number, UnwrapNestedRefs<Album>>(),
state: reactive({
albums: []
albums: [] as Album[]
}),
byId (id: number) {
@ -48,7 +48,7 @@ export const albumStore = {
songStore.byAlbum(album).forEach(song => song.album_cover = album.cover)
// sync to vault
this.byId(album.id).cover = album.cover
this.byId(album.id)!.cover = album.cover
return album.cover
},

View file

@ -20,7 +20,7 @@ new class extends UnitTestCase {
})
it('removes artists by IDs', () => {
const artists = factory<Artist[]>('artist', 3)
const artists = factory<Artist>('artist', 3)
artists.forEach(artist => artistStore.vault.set(artist.id, artist))
artistStore.state.artists = artists
@ -64,19 +64,19 @@ new class extends UnitTestCase {
artistStore.syncWithVault(artist)
expect(artistStore.vault.size).toBe(1)
expect(artistStore.vault.get(artist.id).name).toBe('Pink Floyd')
expect(artistStore.vault.get(artist.id)?.name).toBe('Pink Floyd')
})
it('uploads an image for an artist', async () => {
const artist = factory<Artist>('artist')
artistStore.syncWithVault(artist)
const putMock = this.mock(httpService, 'put').mockResolvedValue({ imageUrl: 'https://foo/img.jpg' })
const putMock = this.mock(httpService, 'put').mockResolvedValue({ imageUrl: 'http://localhost/img.jpg' })
await artistStore.uploadImage(artist, 'data://image')
expect(artist.image).toBe('https://foo/img.jpg')
expect(artist.image).toBe('http://localhost/img.jpg')
expect(putMock).toHaveBeenCalledWith(`artist/${artist.id}/image`, { image: 'data://image' })
expect(artistStore.byId(artist.id).image).toBe('https://foo/img.jpg')
expect(artistStore.byId(artist.id)?.image).toBe('http://localhost/img.jpg')
})
it('resolves an artist', async () => {
@ -92,7 +92,7 @@ new class extends UnitTestCase {
})
it('paginates', async () => {
const artists = factory<Artist[]>('artist', 3)
const artists = factory<Artist>('artist', 3)
this.mock(httpService, 'get').mockResolvedValueOnce({
data: artists,

View file

@ -10,7 +10,7 @@ export const artistStore = {
vault: new Map<number, UnwrapNestedRefs<Artist>>(),
state: reactive({
artists: []
artists: [] as Artist[]
}),
byId (id: number) {
@ -38,7 +38,7 @@ export const artistStore = {
artist.image = (await httpService.put<{ imageUrl: string }>(`artist/${artist.id}/image`, { image })).imageUrl
// sync to vault
this.byId(artist.id).image = artist.image
this.byId(artist.id)!.image = artist.image
return artist.image
},

View file

@ -29,7 +29,7 @@ new class extends UnitTestCase {
})
it('adds songs', () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
favoriteStore.add(songs)
expect(favoriteStore.state.songs).toEqual(songs)
@ -39,14 +39,14 @@ new class extends UnitTestCase {
})
it('removes songs', () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
favoriteStore.state.songs = songs
favoriteStore.remove(songs)
expect(favoriteStore.state.songs).toEqual([])
})
it('likes several songs', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const addMock = this.mock(favoriteStore, 'add')
const postMock = this.mock(httpService, 'post')
@ -57,7 +57,7 @@ new class extends UnitTestCase {
})
it('unlikes several songs', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const removeMock = this.mock(favoriteStore, 'remove')
const postMock = this.mock(httpService, 'post')
@ -68,7 +68,7 @@ new class extends UnitTestCase {
})
it('fetches favorites', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(songs)
await favoriteStore.fetch()

View file

@ -25,12 +25,12 @@ new class extends UnitTestCase {
const artistSyncMock = this.mock(artistStore, 'syncWithVault')
const refreshMock = this.mock(overviewStore, 'refresh')
const mostPlayedSongs = factory<Song[]>('song', 7)
const mostPlayedAlbums = factory<Album[]>('album', 6)
const mostPlayedArtists = factory<Artist[]>('artist', 6)
const recentlyAddedSongs = factory<Song[]>('song', 9)
const recentlyAddedAlbums = factory<Album[]>('album', 6)
const recentlyPlayedSongs = factory<Song[]>('song', 9)
const mostPlayedSongs = factory<Song>('song', 7)
const mostPlayedAlbums = factory<Album>('album', 6)
const mostPlayedArtists = factory<Artist>('artist', 6)
const recentlyAddedSongs = factory<Song>('song', 9)
const recentlyAddedAlbums = factory<Album>('album', 6)
const recentlyPlayedSongs = factory<Song>('song', 9)
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce({
most_played_songs: mostPlayedSongs,
@ -52,12 +52,12 @@ new class extends UnitTestCase {
})
it('refreshes the store', () => {
const mostPlayedSongs = factory<Song[]>('song', 7)
const mostPlayedAlbums = factory<Album[]>('album', 6)
const mostPlayedArtists = factory<Artist[]>('artist', 6)
const recentlyAddedSongs = factory<Song[]>('song', 9)
const recentlyAddedAlbums = factory<Album[]>('album', 6)
const recentlyPlayedSongs = factory<Song[]>('song', 9)
const mostPlayedSongs = factory<Song>('song', 7)
const mostPlayedAlbums = factory<Album>('album', 6)
const mostPlayedArtists = factory<Artist>('artist', 6)
const recentlyAddedSongs = factory<Song>('song', 9)
const recentlyAddedAlbums = factory<Album>('album', 6)
const recentlyPlayedSongs = factory<Song>('song', 9)
const mostPlayedSongsMock = this.mock(songStore, 'getMostPlayed', mostPlayedSongs)
const mostPlayedAlbumsMock = this.mock(albumStore, 'getMostPlayed', mostPlayedAlbums)

View file

@ -7,12 +7,12 @@ import { recentlyPlayedStore } from '@/stores'
export const overviewStore = {
state: reactive({
recentlyPlayed: [],
recentlyAddedSongs: [],
recentlyAddedAlbums: [],
mostPlayedSongs: [],
mostPlayedAlbums: [],
mostPlayedArtists: []
recentlyPlayed: [] as Song[],
recentlyAddedSongs: [] as Song[],
recentlyAddedAlbums: [] as Album[],
mostPlayedSongs: [] as Song[],
mostPlayedAlbums: [] as Album[],
mostPlayedArtists: [] as Artist[]
}),
async init () {

View file

@ -11,7 +11,7 @@ let songs
new class extends UnitTestCase {
protected beforeEach () {
super.beforeEach(() => {
songs = factory<Song[]>('song', 3)
songs = factory<Song>('song', 3)
queueStore.state.songs = reactive(songs)
})
}
@ -40,7 +40,7 @@ new class extends UnitTestCase {
})
it('replaces the whole queue', () => {
const newSongs = factory<Song[]>('song', 2)
const newSongs = factory<Song>('song', 2)
queueStore.replaceQueueWith(newSongs)
expect(queueStore.all).toEqual(newSongs)
@ -89,7 +89,7 @@ new class extends UnitTestCase {
})
it('fetches random songs to queue', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(songs)
const syncMock = this.mock(songStore, 'syncWithVault', songs)
@ -101,7 +101,7 @@ new class extends UnitTestCase {
})
it('fetches random songs to queue with a custom order', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(songs)
const syncMock = this.mock(songStore, 'syncWithVault', songs)

View file

@ -8,7 +8,7 @@ import { recentlyPlayedStore } from '@/stores/recentlyPlayedStore'
new class extends UnitTestCase {
protected test () {
it('fetches the recently played songs', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(songs)
const syncMock = this.mock(songStore, 'syncWithVault', songs)
@ -21,7 +21,7 @@ new class extends UnitTestCase {
it('fetches when attempting to add a new song and the state is empty', async () => {
recentlyPlayedStore.state.songs = []
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const fetchMock = this.mock(recentlyPlayedStore, 'fetch').mockResolvedValue(songs)
await recentlyPlayedStore.add(factory<Song>('song'))
@ -31,7 +31,7 @@ new class extends UnitTestCase {
it('adds a song to the state', async () => {
const newSong = factory<Song>('song')
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const exceptSongs = songs.slice(0, 7)
// We don't want to keep the reference to the original songs
@ -45,7 +45,7 @@ new class extends UnitTestCase {
})
it('deduplicates when adding a song to the state', async () => {
const songs = factory<Song[]>('song', 10)
const songs = factory<Song>('song', 10)
const newSong = songs[1]
const exceptSongs = songs.slice(0, 7)

View file

@ -25,9 +25,9 @@ new class extends UnitTestCase {
protected test () {
it('performs an excerpt search', async () => {
const result: ExcerptSearchResult = {
songs: factory<Song[]>('song', 3),
albums: factory<Album[]>('album', 3),
artists: factory<Artist[]>('artist', 3)
songs: factory<Song>('song', 3),
albums: factory<Album>('album', 3),
artists: factory<Artist>('artist', 3)
}
const getMock = this.mock(httpService, 'get').mockResolvedValue(result)
@ -48,7 +48,7 @@ new class extends UnitTestCase {
})
it('performs a song search', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(songs)
const syncMock = this.mock(songStore, 'syncWithVault', songs)
@ -62,7 +62,7 @@ new class extends UnitTestCase {
})
it('resets the song result state', () => {
searchStore.state.songs = factory<Song[]>('song', 3)
searchStore.state.songs = factory<Song>('song', 3)
searchStore.resetSongResultState()
expect(searchStore.state.songs).toEqual([])
})

View file

@ -73,7 +73,7 @@ new class extends UnitTestCase {
it('matches a song', () => {
const song = factory<Song>('song', { title: 'An amazing song' })
const songs = [song, ...factory<Song[]>('song', 3)]
const songs = [song, ...factory<Song>('song', 3)]
expect(songStore.match('An amazing song', songs)).toEqual(song)
expect(songStore.match('An Amazing Song', songs)).toEqual(song)
@ -103,24 +103,24 @@ new class extends UnitTestCase {
})
it('updates songs', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const result: SongUpdateResult = {
songs: factory<Song[]>('song', 3),
albums: factory<Album[]>('album', 2),
artists: factory<Artist[]>('artist', 2),
songs: factory<Song>('song', 3),
albums: factory<Album>('album', 2),
artists: factory<Artist>('artist', 2),
removed: {
albums: [{
id: 10,
artist_id: 3,
name: 'Removed Album',
cover: 'https://example.com/removed-album.jpg',
cover: 'http://localhost/removed-album.jpg',
created_at: '2020-01-01'
}],
artists: [{
id: 42,
name: 'Removed Artist',
image: 'https://example.com/removed-artist.jpg',
image: 'http://localhost/removed-artist.jpg',
created_at: '2020-01-01'
}]
}
@ -220,7 +220,7 @@ new class extends UnitTestCase {
})
it('fetches for album', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const album = factory<Album>('album', { id: 42 })
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
const syncMock = this.mock(songStore, 'syncWithVault')
@ -232,7 +232,7 @@ new class extends UnitTestCase {
})
it('fetches for artist', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const artist = factory<Artist>('artist', { id: 42 })
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
const syncMock = this.mock(songStore, 'syncWithVault')
@ -244,7 +244,7 @@ new class extends UnitTestCase {
})
it('fetches for playlist', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const playlist = factory<Playlist>('playlist', { id: 42 })
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
const syncMock = this.mock(songStore, 'syncWithVault')
@ -256,7 +256,7 @@ new class extends UnitTestCase {
})
it('paginates', async () => {
const songs = factory<Song[]>('song', 3)
const songs = factory<Song>('song', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce({
data: songs,

View file

@ -34,7 +34,7 @@ new class extends UnitTestCase {
})
it('fetches users', async () => {
const users = factory<User[]>('user', 3)
const users = factory<User>('user', 3)
const getMock = this.mock(httpService, 'get').mockResolvedValue(users)
await userStore.fetch()