Skip to content

Commit

Permalink
test: add tests for playlist folder functionalities
Browse files Browse the repository at this point in the history
  • Loading branch information
phanan committed Sep 10, 2022
1 parent e8a1cde commit 1730e19
Show file tree
Hide file tree
Showing 58 changed files with 779 additions and 296 deletions.
2 changes: 1 addition & 1 deletion app/Http/Controllers/V6/API/PlaylistFolderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function update(PlaylistFolder $playlistFolder, PlaylistFolderUpdateReque
{
$this->authorize('own', $playlistFolder);

return PlaylistFolderResource::make($this->service->updateFolder($playlistFolder, $request->name));
return PlaylistFolderResource::make($this->service->renameFolder($playlistFolder, $request->name));
}

public function destroy(PlaylistFolder $playlistFolder)
Expand Down
2 changes: 1 addition & 1 deletion app/Models/PlaylistFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected static function booted(): void

public function playlists(): HasMany
{
return $this->hasMany(Playlist::class);
return $this->hasMany(Playlist::class, 'folder_id');
}

public function user(): BelongsTo
Expand Down
2 changes: 1 addition & 1 deletion app/Services/PlaylistFolderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function createFolder(User $user, string $name): PlaylistFolder
return $user->playlist_folders()->create(['name' => $name]);
}

public function updateFolder(PlaylistFolder $folder, string $name): PlaylistFolder
public function renameFolder(PlaylistFolder $folder, string $name): PlaylistFolder
{
$folder->update(['name' => $name]);

Expand Down
2 changes: 1 addition & 1 deletion app/Services/PlaylistService.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function removeSongsFromPlaylist(Playlist $playlist, array $songIds): voi
$playlist->songs()->detach($songIds);
}

/** @deprecated */
/** @deprecated since v6.0.0, use add/removeSongs methods instead */
public function populatePlaylist(Playlist $playlist, array $songIds): void
{
$playlist->songs()->sync($songIds);
Expand Down
60 changes: 32 additions & 28 deletions resources/assets/js/App.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
<template>
<Overlay/>
<DialogBox ref="dialog"/>
<MessageToaster ref="toaster"/>

<div id="main" v-if="authenticated">
<Hotkeys/>
<EventListeners/>
<GlobalEventListeners/>
<AppHeader/>
<MainWrapper/>
<AppFooter/>
<SupportKoel/>
<SongContextMenu/>
<AlbumContextMenu/>
<ArtistContextMenu/>
<PlaylistContextMenu/>
<PlaylistFolderContextMenu/>
<CreateNewPlaylistContextMenu/>
</div>

<template v-else>
<div class="login-wrapper">
<LoginForm @loggedin="onUserLoggedIn"/>
</div>
</template>

<SongContextMenu/>
<AlbumContextMenu/>
<ArtistContextMenu/>
<PlaylistFolderContextMenu/>

<DialogBox ref="dialog"/>
<MessageToaster ref="toaster"/>
<div class="login-wrapper" v-else>
<LoginForm @loggedin="onUserLoggedIn"/>
</div>
</template>

<script lang="ts" setup>
Expand All @@ -32,24 +30,29 @@ import { commonStore, preferenceStore as preferences } from '@/stores'
import { authService, playbackService, socketListener, socketService, uploadService } from '@/services'
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
import AppHeader from '@/components/layout/AppHeader.vue'
import AppFooter from '@/components/layout/app-footer/index.vue'
import EventListeners from '@/components/utils/EventListeners.vue'
import Hotkeys from '@/components/utils/HotkeyListener.vue'
import LoginForm from '@/components/auth/LoginForm.vue'
import MainWrapper from '@/components/layout/main-wrapper/index.vue'
import Overlay from '@/components/ui/Overlay.vue'
import AlbumContextMenu from '@/components/album/AlbumContextMenu.vue'
import ArtistContextMenu from '@/components/artist/ArtistContextMenu.vue'
import PlaylistFolderContextMenu from '@/components/playlist/PlaylistFolderContextMenu.vue'
import SongContextMenu from '@/components/song/SongContextMenu.vue'
import DialogBox from '@/components/ui/DialogBox.vue'
import MessageToaster from '@/components/ui/MessageToaster.vue'
import Overlay from '@/components/ui/Overlay.vue'
// Do not dynamic-import app footer, as it contains the <audio> element
// that is necessary to properly initialize the playService and equalizer.
import AppFooter from '@/components/layout/app-footer/index.vue'
const AppHeader = defineAsyncComponent(() => import('@/components/layout/AppHeader.vue'))
const GlobalEventListeners = defineAsyncComponent(() => import('@/components/utils/GlobalEventListeners.vue'))
const Hotkeys = defineAsyncComponent(() => import('@/components/utils/HotkeyListener.vue'))
const LoginForm = defineAsyncComponent(() => import('@/components/auth/LoginForm.vue'))
const MainWrapper = defineAsyncComponent(() => import('@/components/layout/main-wrapper/index.vue'))
const AlbumContextMenu = defineAsyncComponent(() => import('@/components/album/AlbumContextMenu.vue'))
const ArtistContextMenu = defineAsyncComponent(() => import('@/components/artist/ArtistContextMenu.vue'))
const PlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/PlaylistContextMenu.vue'))
const PlaylistFolderContextMenu = defineAsyncComponent(() => import('@/components/playlist/PlaylistFolderContextMenu.vue'))
const SongContextMenu = defineAsyncComponent(() => import('@/components/song/SongContextMenu.vue'))
const CreateNewPlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/CreateNewPlaylistContextMenu.vue'))
const SupportKoel = defineAsyncComponent(() => import('@/components/meta/SupportKoel.vue'))
const dialog = ref<InstanceType<typeof DialogBox>>()
const toaster = ref<InstanceType<typeof MessageToast>>()
const toaster = ref<InstanceType<typeof MessageToaster>>()
const authenticated = ref(false)
/**
Expand All @@ -61,9 +64,9 @@ const requestNotificationPermission = async () => {
}
}
const onUserLoggedIn = () => {
const onUserLoggedIn = async () => {
authenticated.value = true
init()
await init()
}
onMounted(async () => {
Expand Down Expand Up @@ -149,6 +152,7 @@ provide(MessageToasterKey, toaster)
display: flex;
height: 100vh;
flex-direction: column;
justify-content: flex-end;
}
.login-wrapper {
Expand Down
4 changes: 4 additions & 0 deletions resources/assets/js/__tests__/UnitTestCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,15 @@ export default abstract class UnitTestCase {
options.global = options.global || {}
options.global.provide = options.global.provide || {}

// @ts-ignore
if (!options.global.provide?.hasOwnProperty(DialogBoxKey)) {
// @ts-ignore
options.global.provide[DialogBoxKey] = DialogBoxStub
}

// @ts-ignore
if (!options.global.provide?.hasOwnProperty(MessageToasterKey)) {
// @ts-ignore
options.global.provide[MessageToasterKey] = MessageToasterStub
}

Expand Down
2 changes: 2 additions & 0 deletions resources/assets/js/__tests__/factory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import interactionFactory from '@/__tests__/factory/interactionFactory'
import smartPlaylistRuleFactory from '@/__tests__/factory/smartPlaylistRuleFactory'
import smartPlaylistRuleGroupFactory from '@/__tests__/factory/smartPlaylistRuleGroupFactory'
import playlistFactory, { states as playlistStates } from '@/__tests__/factory/playlistFactory'
import playlistFolderFactory from '@/__tests__/factory/playlistFolderFactory'
import userFactory, { states as userStates } from '@/__tests__/factory/userFactory'
import albumTrackFactory from '@/__tests__/factory/albumTrackFactory'
import albumInfoFactory from '@/__tests__/factory/albumInfoFactory'
Expand All @@ -24,4 +25,5 @@ export default factory
.define('smart-playlist-rule', faker => smartPlaylistRuleFactory(faker))
.define('smart-playlist-rule-group', faker => smartPlaylistRuleGroupFactory(faker))
.define('playlist', faker => playlistFactory(faker), playlistStates)
.define('playlist-folder', faker => playlistFolderFactory(faker))
.define('user', faker => userFactory(faker), userStates)
10 changes: 7 additions & 3 deletions resources/assets/js/__tests__/factory/playlistFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import { Faker } from '@faker-js/faker'
export default (faker: Faker): Playlist => ({
type: 'playlists',
id: faker.datatype.number(),
folder_id: faker.datatype.uuid(),
name: faker.random.word(),
is_smart: false,
rules: []
})

export const states: Record<string, () => Omit<Partial<Playlist>, 'type'>> = {
smart: faker => ({
export const states: Record<string, (faker: Faker) => Omit<Partial<Playlist>, 'type'>> = {
smart: _ => ({
is_smart: true,
rules: [
factory<SmartPlaylistRule>('smart-playlist-rule')
factory<SmartPlaylistRuleGroup>('smart-playlist-rule-group')
]
}),
orphan: _ => ({
folder_id: null
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Faker } from '@faker-js/faker'

export default (faker: Faker): PlaylistFolder => ({
type: 'playlist-folders',
id: faker.datatype.uuid(),
name: faker.random.word()
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { Faker } from '@faker-js/faker'
export default (faker: Faker): SmartPlaylistRule => ({
id: faker.datatype.number(),
model: faker.random.arrayElement<SmartPlaylistModel['name']>(['title', 'artist.name', 'album.name']),
operator: faker.random.arrayElement<SmartPlaylistOperator['name']>(['is', 'contains', 'isNot']),
operator: faker.random.arrayElement<SmartPlaylistOperator['operator']>(['is', 'contains', 'isNot']),
value: [faker.random.word()]
})
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ new class extends UnitTestCase {
})

const rendered = this.render(AlbumContextMenu)
eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 69 }, album)
eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 }, album)
await this.tick(2)

return rendered
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Vitest Snapshot v1

exports[`renders 1`] = `
<nav class="album-menu menu context-menu" style="top: 69px; left: 420px;" tabindex="0" data-testid="album-context-menu">
<nav class="album-menu menu context-menu" style="top: 42px; left: 420px;" tabindex="0" data-testid="album-context-menu">
<ul>
<li data-testid="play">Play All</li>
<li data-testid="shuffle">Shuffle All</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ new class extends UnitTestCase {
})

const rendered = this.render(ArtistContextMenu)
eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 69 }, artist)
eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 }, artist)
await this.tick(2)

return rendered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ const download = () => trigger(() => downloadService.fromArtist(artist.value))
eventBus.on('ARTIST_CONTEXT_MENU_REQUESTED', async (e: MouseEvent, _artist: Artist) => {
artist.value = _artist
open(e.pageY, e.pageX, { _artist })
await open(e.pageY, e.pageX, { _artist })
})
</script>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Vitest Snapshot v1

exports[`renders 1`] = `
<nav class="artist-menu menu context-menu" style="top: 69px; left: 420px;" tabindex="0" data-testid="artist-context-menu">
<nav class="artist-menu menu context-menu" style="top: 42px; left: 420px;" tabindex="0" data-testid="artist-context-menu">
<ul>
<li data-testid="play">Play All</li>
<li data-testid="shuffle">Shuffle All</li>
Expand Down
34 changes: 27 additions & 7 deletions resources/assets/js/components/layout/ModalWrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import { it } from 'vitest'
import { waitFor } from '@testing-library/vue'
import factory from '@/__tests__/factory'
import { eventBus } from '@/utils'
import { it } from 'vitest'
import { EventName } from '@/config'
import UnitTestCase from '@/__tests__/UnitTestCase'
import ModalWrapper from './ModalWrapper.vue'

new class extends UnitTestCase {
protected test () {
it.each<[string, EventName, User | Song | Playlist | any]>([
it.each<[string, EventName, User | Song[] | Playlist | PlaylistFolder | undefined]>([
['add-user-form', 'MODAL_SHOW_ADD_USER_FORM', undefined],
['edit-user-form', 'MODAL_SHOW_EDIT_USER_FORM', factory('user')],
['edit-song-form', 'MODAL_SHOW_EDIT_SONG_FORM', [factory('song')]],
['edit-user-form', 'MODAL_SHOW_EDIT_USER_FORM', factory<User>('user')],
['edit-song-form', 'MODAL_SHOW_EDIT_SONG_FORM', [factory<Song>('song')]],
['create-playlist-form', 'MODAL_SHOW_CREATE_PLAYLIST_FORM', undefined],
['create-playlist-folder-form', 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM', undefined],
['edit-playlist-folder-form', 'MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM', factory<PlaylistFolder>('playlist-folder')],
['create-smart-playlist-form', 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', undefined],
['edit-smart-playlist-form', 'MODAL_SHOW_EDIT_SMART_PLAYLIST_FORM', factory('playlist')],
['edit-playlist-form', 'MODAL_SHOW_EDIT_PLAYLIST_FORM', factory<Playlist>('playlist')],
['edit-smart-playlist-form', 'MODAL_SHOW_EDIT_PLAYLIST_FORM', factory<Playlist>('playlist', { is_smart: true })],
['about-koel', 'MODAL_SHOW_ABOUT_KOEL', undefined]
])('shows %s modal', async (modalName: string, eventName: EventName, eventParams?: any) => {
const { findByTestId } = this.render(ModalWrapper)
const { getByTestId } = this.render(ModalWrapper, {
global: {
stubs: {
AddUserForm: this.stub('add-user-form'),
EditUserForm: this.stub('edit-user-form'),
EditSongForm: this.stub('edit-song-form'),
CreatePlaylistForm: this.stub('create-playlist-form'),
CreatePlaylistFolderForm: this.stub('create-playlist-folder-form'),
EditPlaylistFolderForm: this.stub('edit-playlist-folder-form'),
CreateSmartPlaylistForm: this.stub('create-smart-playlist-form'),
EditPlaylistForm: this.stub('edit-playlist-form'),
EditSmartPlaylistForm: this.stub('edit-smart-playlist-form'),
AboutKoel: this.stub('about-koel')
}
}
})

eventBus.emit(eventName, eventParams)

findByTestId(modalName)
await waitFor(() => getByTestId(modalName))
})
}
}
13 changes: 6 additions & 7 deletions resources/assets/js/components/layout/ModalWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const editSongFormInitialTab = ref<EditSongFormTabName>('details')
provideReadonly(PlaylistKey, playlistToEdit, false)
provideReadonly(UserKey, userToEdit)
provideReadonly(PlaylistFolderKey, playlistFolderToEdit, true, (name: string) => playlistFolderToEdit.value!.name = name)
provideReadonly(PlaylistFolderKey, playlistFolderToEdit, true, (name: string) => {
playlistFolderToEdit.value!.name = name
})
provideReadonly(SongsKey, songsToEdit, false)
provideReadonly(EditSongFormInitialTabKey, editSongFormInitialTab)
Expand All @@ -66,12 +70,7 @@ eventBus.on({
'MODAL_SHOW_EDIT_PLAYLIST_FORM': (playlist: Playlist) => {
playlistToEdit.value = playlist
showingModalName.value = 'edit-playlist-form'
},
'MODAL_SHOW_EDIT_SMART_PLAYLIST_FORM': (playlist: Playlist) => {
playlistToEdit.value = playlist
showingModalName.value = 'edit-smart-playlist-form'
showingModalName.value = playlist.is_smart ? 'edit-smart-playlist-form' : 'edit-playlist-form'
},
'MODAL_SHOW_EDIT_USER_FORM': (user: User) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// Vitest Snapshot v1

exports[`renders with a song 1`] = `
<div class="middle-pane" data-testid="footer-middle-pane">
<div id="progressPane" class="progress">
<h3 class="title">Fahrstuhl to Heaven</h3>
<p class="meta"><a href="/#!/artist/3" class="artist">Led Zeppelin</a> – <a href="/#!/album/4" class="album">Led Zeppelin IV</a></p>
<div class="plyr"><audio controls="" crossorigin="anonymous"></audio></div>
<div class="middle-pane" data-testid="footer-middle-pane" data-v-2ff4ca72="">
<div id="progressPane" class="progress" data-v-2ff4ca72="">
<h3 class="title" data-v-2ff4ca72="">Fahrstuhl to Heaven</h3>
<p class="meta" data-v-2ff4ca72=""><a href="/#!/artist/3" class="artist" data-v-2ff4ca72="">Led Zeppelin</a> – <a href="/#!/album/4" class="album" data-v-2ff4ca72="">Led Zeppelin IV</a></p>
<div class="plyr" data-v-2ff4ca72=""><audio controls="" crossorigin="anonymous" data-v-2ff4ca72=""></audio></div>
</div>
</div>
`;

exports[`renders without a song 1`] = `
<div class="middle-pane" data-testid="footer-middle-pane">
<div id="progressPane" class="progress">
<div class="middle-pane" data-testid="footer-middle-pane" data-v-2ff4ca72="">
<div id="progressPane" class="progress" data-v-2ff4ca72="">
<!--v-if-->
<div class="plyr"><audio controls="" crossorigin="anonymous"></audio></div>
<div class="plyr" data-v-2ff4ca72=""><audio controls="" crossorigin="anonymous" data-v-2ff4ca72=""></audio></div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ const onQueueDragOver = (event: DragEvent) => {
event.preventDefault()
event.dataTransfer!.dropEffect = 'move'
queueMenuItemEl.value!.classList.add('droppable')
queueMenuItemEl.value?.classList.add('droppable')
}
const onQueueDragLeave = () => queueMenuItemEl.value!.classList.remove('droppable')
const onQueueDragLeave = () => queueMenuItemEl.value?.classList.remove('droppable')
const onQueueDrop = async (event: DragEvent) => {
queueMenuItemEl.value!.classList.remove('droppable')
queueMenuItemEl.value?.classList.remove('droppable')
if (!acceptsDrop(event)) return false
Expand Down
Loading

0 comments on commit 1730e19

Please sign in to comment.