mirror of
https://github.com/koel/koel
synced 2024-11-28 15:00:42 +00:00
feat(test): add and fix playlistStore tests
This commit is contained in:
parent
dcc99b9a03
commit
d42e1e84a3
7 changed files with 159 additions and 53 deletions
|
@ -3,6 +3,7 @@ import artistFactory, { states as artistStates } from '@/__tests__/factory/artis
|
||||||
import songFactory, { states as songStates } from '@/__tests__/factory/songFactory'
|
import songFactory, { states as songStates } from '@/__tests__/factory/songFactory'
|
||||||
import albumFactory, { states as albumStates } from './albumFactory'
|
import albumFactory, { states as albumStates } from './albumFactory'
|
||||||
import smartPlaylistRuleFactory from '@/__tests__/factory/smartPlaylistRuleFactory'
|
import smartPlaylistRuleFactory from '@/__tests__/factory/smartPlaylistRuleFactory'
|
||||||
|
import smartPlaylistRuleGroupFactory from '@/__tests__/factory/smartPlaylistRuleGroupFactory'
|
||||||
import playlistFactory, { states as playlistStates } from '@/__tests__/factory/playlistFactory'
|
import playlistFactory, { states as playlistStates } from '@/__tests__/factory/playlistFactory'
|
||||||
import userFactory, { states as userStates } from '@/__tests__/factory/userFactory'
|
import userFactory, { states as userStates } from '@/__tests__/factory/userFactory'
|
||||||
import albumTrackFactory from '@/__tests__/factory/albumTrackFactory'
|
import albumTrackFactory from '@/__tests__/factory/albumTrackFactory'
|
||||||
|
@ -10,7 +11,7 @@ import albumInfoFactory from '@/__tests__/factory/albumInfoFactory'
|
||||||
import artistInfoFactory from '@/__tests__/factory/artistInfoFactory'
|
import artistInfoFactory from '@/__tests__/factory/artistInfoFactory'
|
||||||
import youTubeVideoFactory from '@/__tests__/factory/youTubeVideoFactory'
|
import youTubeVideoFactory from '@/__tests__/factory/youTubeVideoFactory'
|
||||||
|
|
||||||
factory
|
export default factory
|
||||||
.define('artist', faker => artistFactory(faker), artistStates)
|
.define('artist', faker => artistFactory(faker), artistStates)
|
||||||
.define('artist-info', faker => artistInfoFactory(faker))
|
.define('artist-info', faker => artistInfoFactory(faker))
|
||||||
.define('album', faker => albumFactory(faker), albumStates)
|
.define('album', faker => albumFactory(faker), albumStates)
|
||||||
|
@ -19,7 +20,6 @@ factory
|
||||||
.define('song', faker => songFactory(faker), songStates)
|
.define('song', faker => songFactory(faker), songStates)
|
||||||
.define('video', faker => youTubeVideoFactory(faker))
|
.define('video', faker => youTubeVideoFactory(faker))
|
||||||
.define('smart-playlist-rule', faker => smartPlaylistRuleFactory(faker))
|
.define('smart-playlist-rule', faker => smartPlaylistRuleFactory(faker))
|
||||||
|
.define('smart-playlist-rule-group', faker => smartPlaylistRuleGroupFactory(faker))
|
||||||
.define('playlist', faker => playlistFactory(faker), playlistStates)
|
.define('playlist', faker => playlistFactory(faker), playlistStates)
|
||||||
.define('user', faker => userFactory(faker), userStates)
|
.define('user', faker => userFactory(faker), userStates)
|
||||||
|
|
||||||
export default factory
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Faker } from '@faker-js/faker'
|
||||||
|
import factory from 'factoria'
|
||||||
|
|
||||||
|
export default (faker: Faker): SmartPlaylistRuleGroup => ({
|
||||||
|
id: faker.datatype.number(),
|
||||||
|
rules: factory<SmartPlaylistRule[]>('smart-playlist-rule', 3)
|
||||||
|
})
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
v-model="mutatedPlaylist.name"
|
v-model="name"
|
||||||
v-koel-focus
|
v-koel-focus
|
||||||
data-testid="inline-playlist-name-input"
|
data-testid="inline-playlist-name-input"
|
||||||
name="name"
|
name="name"
|
||||||
|
@ -15,44 +15,42 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, toRefs } from 'vue'
|
import { reactive, ref, toRefs } from 'vue'
|
||||||
import { playlistStore } from '@/stores'
|
import { playlistStore } from '@/stores'
|
||||||
import { alerts } from '@/utils'
|
import { alerts, logger } from '@/utils'
|
||||||
|
|
||||||
const props = defineProps<{ playlist: Playlist }>()
|
const props = defineProps<{ playlist: Playlist }>()
|
||||||
const { playlist } = toRefs(props)
|
const { playlist } = toRefs(props)
|
||||||
|
|
||||||
const updating = ref(false)
|
let updating = false
|
||||||
|
|
||||||
const mutatedPlaylist = reactive<Playlist>(Object.assign({}, playlist.value))
|
const mutablePlaylist = reactive<Playlist>(Object.assign({}, playlist.value))
|
||||||
|
const name = ref(mutablePlaylist.name)
|
||||||
|
|
||||||
const emit = defineEmits(['updated', 'cancelled'])
|
const emit = defineEmits(['updated', 'cancelled'])
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
mutatedPlaylist.name = mutatedPlaylist.name.trim()
|
if (!name.value || name.value === playlist.value.name) {
|
||||||
|
|
||||||
if (!mutatedPlaylist.name) {
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mutatedPlaylist.name === playlist.value.name) {
|
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent duplicate updating from Enter and Blur
|
// prevent duplicate updating from Enter and Blur
|
||||||
if (updating.value) {
|
if (updating) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updating.value = true
|
updating = true
|
||||||
|
|
||||||
await playlistStore.update(mutatedPlaylist)
|
try {
|
||||||
alerts.success(`Playlist "${mutatedPlaylist.name}" updated.`)
|
await playlistStore.update(mutablePlaylist, { name: name.value })
|
||||||
emit('updated', mutatedPlaylist)
|
alerts.success(`Playlist "${name}" updated.`)
|
||||||
|
emit('updated', name)
|
||||||
|
} catch (error) {
|
||||||
|
alerts.error('Something went wrong. Please try again.')
|
||||||
|
logger.error(error)
|
||||||
|
} finally {
|
||||||
|
updating = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => emit('cancelled')
|
||||||
mutatedPlaylist.name = playlist.value.name
|
|
||||||
emit('cancelled')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -117,8 +117,8 @@ const openContextMenu = async (event: MouseEvent) => {
|
||||||
|
|
||||||
const cancelEditing = () => (editing.value = false)
|
const cancelEditing = () => (editing.value = false)
|
||||||
|
|
||||||
const onPlaylistNameUpdated = (mutatedPlaylist: Playlist) => {
|
const onPlaylistNameUpdated = (name: string) => {
|
||||||
playlist.value.name = mutatedPlaylist.name
|
playlist.value.name = name
|
||||||
editing.value = false
|
editing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>
|
<label>
|
||||||
Name
|
Name
|
||||||
<input v-model="mutatedPlaylist.name" v-koel-focus name="name" required type="text">
|
<input v-model="mutablePlaylist.name" v-koel-focus name="name" required type="text">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row rules">
|
<div class="form-row rules">
|
||||||
<RuleGroup
|
<RuleGroup
|
||||||
v-for="(group, index) in mutatedPlaylist.rules"
|
v-for="(group, index) in mutablePlaylist.rules"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
:group="group"
|
:group="group"
|
||||||
:isFirstGroup="index === 0"
|
:isFirstGroup="index === 0"
|
||||||
|
@ -43,18 +43,18 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { computed, reactive, watch } from 'vue'
|
import { computed, reactive, watch } from 'vue'
|
||||||
import { cloneDeep, isEqual } from 'lodash'
|
import { cloneDeep, isEqual } from 'lodash'
|
||||||
import { playlistStore } from '@/stores'
|
import { playlistStore } from '@/stores'
|
||||||
import { alerts, eventBus, requireInjection } from '@/utils'
|
import { alerts, eventBus, logger, requireInjection } from '@/utils'
|
||||||
import { useSmartPlaylistForm } from '@/components/playlist/smart-playlist/useSmartPlaylistForm'
|
import { useSmartPlaylistForm } from '@/components/playlist/smart-playlist/useSmartPlaylistForm'
|
||||||
import { PlaylistKey } from '@/symbols'
|
import { PlaylistKey } from '@/symbols'
|
||||||
|
|
||||||
const [playlist] = requireInjection(PlaylistKey)
|
const [playlist] = requireInjection(PlaylistKey)
|
||||||
|
|
||||||
let mutatedPlaylist: Playlist
|
let mutablePlaylist: Playlist
|
||||||
|
|
||||||
watch(playlist, () => (mutatedPlaylist = reactive(cloneDeep(playlist.value))), { immediate: true })
|
watch(playlist, () => (mutablePlaylist = reactive(cloneDeep(playlist.value))), { immediate: true })
|
||||||
|
|
||||||
const isPristine = computed(() => {
|
const isPristine = computed(() => {
|
||||||
return isEqual(mutatedPlaylist.rules, playlist.value.rules) && mutatedPlaylist.name.trim() === playlist.value.name
|
return isEqual(mutablePlaylist.rules, playlist.value.rules) && mutablePlaylist.name.trim() === playlist.value.name
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -66,7 +66,7 @@ const {
|
||||||
loading,
|
loading,
|
||||||
addGroup,
|
addGroup,
|
||||||
onGroupChanged
|
onGroupChanged
|
||||||
} = useSmartPlaylistForm(mutatedPlaylist.rules)
|
} = useSmartPlaylistForm(mutablePlaylist.rules)
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
@ -82,12 +82,22 @@ const maybeClose = () => {
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
mutatedPlaylist.rules = collectedRuleGroups.value
|
mutablePlaylist.rules = collectedRuleGroups.value
|
||||||
await playlistStore.update(mutatedPlaylist)
|
|
||||||
Object.assign(playlist.value, mutatedPlaylist)
|
try {
|
||||||
loading.value = false
|
await playlistStore.update(playlist.value, {
|
||||||
alerts.success(`Playlist "${playlist.value.name}" updated.`)
|
name: mutablePlaylist.name,
|
||||||
eventBus.emit('SMART_PLAYLIST_UPDATED', playlist.value)
|
rules: mutablePlaylist.rules
|
||||||
close()
|
})
|
||||||
|
|
||||||
|
alerts.success(`Playlist "${playlist.value.name}" updated.`)
|
||||||
|
eventBus.emit('SMART_PLAYLIST_UPDATED', playlist.value)
|
||||||
|
close()
|
||||||
|
} catch (error) {
|
||||||
|
alerts.error('Something went wrong. Please try again.')
|
||||||
|
logger.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
import factory from '@/__tests__/factory'
|
import factory from '@/__tests__/factory'
|
||||||
import { playlistStore } from '@/stores'
|
import { playlistStore } from '@/stores'
|
||||||
import { expect, it } from 'vitest'
|
import { expect, it } from 'vitest'
|
||||||
|
import { Cache, httpService } from '@/services'
|
||||||
|
|
||||||
const ruleGroups: SmartPlaylistRuleGroup[] = [
|
const ruleGroups: SmartPlaylistRuleGroup[] = [
|
||||||
{
|
{
|
||||||
|
@ -70,12 +71,102 @@ new class extends UnitTestCase {
|
||||||
it('sets up a smart playlist with properly unserialized rules', () => {
|
it('sets up a smart playlist with properly unserialized rules', () => {
|
||||||
const playlist = factory<Playlist>('playlist', {
|
const playlist = factory<Playlist>('playlist', {
|
||||||
is_smart: true,
|
is_smart: true,
|
||||||
rules: serializedRuleGroups as unknown as SmartPlaylistRuleGroup[]
|
rules: serializedRuleGroups as SmartPlaylistRuleGroup[]
|
||||||
})
|
})
|
||||||
|
|
||||||
playlistStore.setupSmartPlaylist(playlist)
|
playlistStore.setupSmartPlaylist(playlist)
|
||||||
|
|
||||||
expect(playlist.rules).toEqual(ruleGroups)
|
expect(playlist.rules).toEqual(ruleGroups)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores a playlist', async () => {
|
||||||
|
const songs = factory<Song[]>('song', 3)
|
||||||
|
const playlist = factory<Playlist>('playlist')
|
||||||
|
const postMock = this.mock(httpService, 'post').mockResolvedValue(playlist)
|
||||||
|
const serializeMock = this.mock(playlistStore, 'serializeSmartPlaylistRulesForStorage', null)
|
||||||
|
|
||||||
|
await playlistStore.store('New Playlist', songs, [])
|
||||||
|
|
||||||
|
expect(postMock).toHaveBeenCalledWith('playlist', {
|
||||||
|
name: 'New Playlist',
|
||||||
|
songs: songs.map(song => song.id),
|
||||||
|
rules: null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(serializeMock).toHaveBeenCalledWith([])
|
||||||
|
expect(playlistStore.state.playlists).toHaveLength(1)
|
||||||
|
expect(playlistStore.state.playlists[0]).toEqual(playlist)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes a playlist', async () => {
|
||||||
|
const playlist = factory<Playlist>('playlist', { id: 12 })
|
||||||
|
const deleteMock = this.mock(httpService, 'delete')
|
||||||
|
playlistStore.state.playlists = [factory<Playlist>('playlist'), playlist]
|
||||||
|
|
||||||
|
await playlistStore.delete(playlist)
|
||||||
|
|
||||||
|
expect(deleteMock).toHaveBeenCalledWith('playlists/12')
|
||||||
|
expect(playlistStore.state.playlists).toHaveLength(1)
|
||||||
|
expect(playlistStore.byId(playlist.id)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds songs to a playlist', async () => {
|
||||||
|
const playlist = factory<Playlist>('playlist', { id: 12 })
|
||||||
|
const songs = factory<Song[]>('song', 3)
|
||||||
|
const postMock = this.mock(httpService, 'post').mockResolvedValue(playlist)
|
||||||
|
const invalidateMock = this.mock(Cache, 'invalidate')
|
||||||
|
|
||||||
|
await playlistStore.addSongs(playlist, songs)
|
||||||
|
|
||||||
|
expect(postMock).toHaveBeenCalledWith('playlists/12/songs', { songs: songs.map(song => song.id) })
|
||||||
|
expect(invalidateMock).toHaveBeenCalledWith(['playlist.songs', 12])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes songs from a playlist', async () => {
|
||||||
|
const playlist = factory<Playlist>('playlist', { id: 12 })
|
||||||
|
const songs = factory<Song[]>('song', 3)
|
||||||
|
const deleteMock = this.mock(httpService, 'delete').mockResolvedValue(playlist)
|
||||||
|
const invalidateMock = this.mock(Cache, 'invalidate')
|
||||||
|
|
||||||
|
await playlistStore.removeSongs(playlist, songs)
|
||||||
|
|
||||||
|
expect(deleteMock).toHaveBeenCalledWith('playlists/12/songs', { songs: songs.map(song => song.id) })
|
||||||
|
expect(invalidateMock).toHaveBeenCalledWith(['playlist.songs', 12])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not modify a smart playlist content', async () => {
|
||||||
|
const playlist = factory.states('smart')<Playlist>('playlist')
|
||||||
|
const postMock = this.mock(httpService, 'post')
|
||||||
|
|
||||||
|
await playlistStore.addSongs(playlist, factory<Song[]>('song', 3))
|
||||||
|
expect(postMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
await playlistStore.removeSongs(playlist, factory<Song[]>('song', 3))
|
||||||
|
expect(postMock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates a standard playlist', async () => {
|
||||||
|
const playlist = factory<Playlist>('playlist', { id: 12 })
|
||||||
|
const putMock = this.mock(httpService, 'put').mockResolvedValue(playlist)
|
||||||
|
|
||||||
|
await playlistStore.update(playlist, { name: 'Foo' })
|
||||||
|
|
||||||
|
expect(putMock).toHaveBeenCalledWith('playlists/12', { name: 'Foo', rules: null })
|
||||||
|
expect(playlist.name).toBe('Foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates a smart playlist', async () => {
|
||||||
|
const playlist = factory.states('smart')<Playlist>('playlist', { id: 12 })
|
||||||
|
const rules = factory<SmartPlaylistRuleGroup[]>('smart-playlist-rule-group', 2)
|
||||||
|
const serializeMock = this.mock(playlistStore, 'serializeSmartPlaylistRulesForStorage', ['Whatever'])
|
||||||
|
const putMock = this.mock(httpService, 'put').mockResolvedValue(playlist)
|
||||||
|
const invalidateMock = this.mock(Cache, 'invalidate')
|
||||||
|
|
||||||
|
await playlistStore.update(playlist, { name: 'Foo', rules })
|
||||||
|
|
||||||
|
expect(serializeMock).toHaveBeenCalledWith(rules)
|
||||||
|
expect(putMock).toHaveBeenCalledWith('playlists/12', { name: 'Foo', rules: ['Whatever'] })
|
||||||
|
expect(invalidateMock).toHaveBeenCalledWith(['playlist.songs', 12])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { differenceBy, orderBy } from 'lodash'
|
import { differenceBy, orderBy } from 'lodash'
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
import { logger } from '@/utils'
|
import { logger } from '@/utils'
|
||||||
import { Cache, httpService } from '@/services'
|
import { Cache, httpService } from '@/services'
|
||||||
import models from '@/config/smart-playlist/models'
|
import models from '@/config/smart-playlist/models'
|
||||||
|
@ -13,11 +12,7 @@ export const playlistStore = {
|
||||||
|
|
||||||
init (playlists: Playlist[]) {
|
init (playlists: Playlist[]) {
|
||||||
this.state.playlists = this.sort(playlists)
|
this.state.playlists = this.sort(playlists)
|
||||||
this.state.playlists.forEach(playlist => this.setupPlaylist(playlist))
|
this.state.playlists.forEach(playlist => playlist.is_smart && this.setupSmartPlaylist(playlist))
|
||||||
},
|
|
||||||
|
|
||||||
setupPlaylist (playlist: Playlist) {
|
|
||||||
playlist.is_smart && this.setupSmartPlaylist(playlist)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,10 +38,12 @@ export const playlistStore = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async store (name: string, songs: Song[] = [], rules: SmartPlaylistRuleGroup[] = []) {
|
async store (name: string, songs: Song[] = [], rules: SmartPlaylistRuleGroup[] = []) {
|
||||||
const songIds = songs.map(song => song.id)
|
const playlist = await httpService.post<Playlist>('playlist', {
|
||||||
const serializedRules = this.serializeSmartPlaylistRulesForStorage(rules)
|
name,
|
||||||
|
songs: songs.map(song => song.id),
|
||||||
|
rules: this.serializeSmartPlaylistRulesForStorage(rules)
|
||||||
|
})
|
||||||
|
|
||||||
const playlist = await httpService.post<Playlist>('playlist', { name, songs: songIds, rules: serializedRules })
|
|
||||||
this.state.playlists.push(playlist)
|
this.state.playlists.push(playlist)
|
||||||
this.state.playlists = this.sort(this.state.playlists)
|
this.state.playlists = this.sort(this.state.playlists)
|
||||||
|
|
||||||
|
@ -80,11 +77,14 @@ export const playlistStore = {
|
||||||
return playlist
|
return playlist
|
||||||
},
|
},
|
||||||
|
|
||||||
async update (playlist: Playlist) {
|
async update (playlist: Playlist, data: Partial<Pick<Playlist, 'name' | 'rules'>>) {
|
||||||
const serializedRules = this.serializeSmartPlaylistRulesForStorage(playlist.rules)
|
await httpService.put(`playlists/${playlist.id}`, {
|
||||||
await httpService.put(`playlists/${playlist.id}`, { name: playlist.name, rules: serializedRules })
|
name: data.name,
|
||||||
|
rules: this.serializeSmartPlaylistRulesForStorage(data.rules || [])
|
||||||
|
})
|
||||||
|
|
||||||
playlist.is_smart && Cache.invalidate(['playlist.songs', playlist.id])
|
playlist.is_smart && Cache.invalidate(['playlist.songs', playlist.id])
|
||||||
|
Object.assign(playlist, data)
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue