chore: add missing tests

This commit is contained in:
Phan An 2024-04-23 17:20:40 +02:00
parent 221e46c1b9
commit bfc807cfb9
20 changed files with 215 additions and 29 deletions

View file

@ -147,6 +147,8 @@ export default abstract class UnitTestCase {
customer_email: 'Koel Plus',
product_id: 'koel-plus',
}
return this
}
protected disablePlusEdition () {
@ -157,6 +159,8 @@ export default abstract class UnitTestCase {
customer_email: '',
product_id: '',
}
return this
}
protected stub (testId = 'stub') {

View file

@ -30,6 +30,9 @@ import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
const { handleHttpError } = useErrorHandler()
const { toastSuccess } = useMessageToaster()
const emit = defineEmits<{ (e: 'cancel'): void }>()
const email = ref('')
const loading = ref(false)
@ -43,9 +46,9 @@ const requestResetPasswordLink = async () => {
try {
loading.value = true
await authService.requestResetPasswordLink(email.value)
useMessageToaster().toastSuccess('Password reset link sent. Please check your email.')
toastSuccess('Password reset link sent. Please check your email.')
} catch (error: unknown) {
useErrorHandler().handleHttpError(error, { 404: 'No user with this email address found.' })
handleHttpError(error, { 404: 'No user with this email address found.' })
} finally {
loading.value = false
}

View file

@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/vue'
import { expect, it, Mock } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { authService } from '@/services'
import { logger } from '@/utils'
import LoginFrom from './LoginForm.vue'
new class extends UnitTestCase {
@ -25,12 +26,14 @@ new class extends UnitTestCase {
})
it('fails to log in', async () => {
const mock = this.mock(authService, 'login').mockRejectedValue(new Error('Unauthenticated'))
const mock = this.mock(authService, 'login').mockRejectedValue('Unauthenticated')
const logMock = this.mock(logger, 'error')
const { emitted } = await this.submitForm(mock)
await this.tick()
expect(emitted().loggedin).toBeFalsy()
expect(screen.getByTestId('login-form').classList.contains('error')).toBe(true)
expect(logMock).toHaveBeenCalledWith('Unauthenticated')
})
it('shows forgot password form', async () => {

View file

@ -0,0 +1,38 @@
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { commonStore } from '@/stores'
import AboutKoelButton from './AboutKoelButton.vue'
new class extends UnitTestCase {
protected test () {
it('shows no notification when no new available available', () => {
commonStore.state.current_version = '1.0.0'
commonStore.state.latest_version = '1.0.0'
this.beAdmin().render(AboutKoelButton)
expect(screen.queryByTitle('New version available!')).toBeNull()
expect(screen.queryByTestId('new-version-indicator')).toBeNull()
screen.getByTitle('About Koel')
})
it('shows notification when new version available', () => {
commonStore.state.current_version = '1.0.0'
commonStore.state.latest_version = '1.0.1'
this.beAdmin().render(AboutKoelButton)
screen.getByTitle('New version available!')
screen.getByTestId('new-version-indicator')
})
it('doesn\'t show notification to non-admin users', () => {
commonStore.state.current_version = '1.0.0'
commonStore.state.latest_version = '1.0.1'
this.be().render(AboutKoelButton)
expect(screen.queryByTitle('New version available!')).toBeNull()
expect(screen.queryByTestId('new-version-indicator')).toBeNull()
screen.getByTitle('About Koel')
})
}
}

View file

@ -8,6 +8,7 @@
<span
v-if="shouldNotifyNewVersion"
class="absolute w-[10px] aspect-square right-px top-px rounded-full bg-k-highlight"
data-testid="new-version-indicator"
/>
</ExtraDrawerButton>
</template>

View file

@ -0,0 +1,18 @@
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { eventBus } from '@/utils'
import LogoutButton from './LogoutButton.vue'
new class extends UnitTestCase {
protected test () {
it('emits the logout event', async () => {
const emitMock = this.mock(eventBus, 'emit')
this.render(LogoutButton)
await this.user.click(screen.getByRole('button'))
expect(emitMock).toHaveBeenCalledWith('LOG_OUT')
})
}
}

View file

@ -35,7 +35,7 @@ const current = ref(false)
const { onRouteChanged } = useRouter()
if (screen) {
onRouteChanged(route => current.value = route.screen === props.screen)
onRouteChanged(route => (current.value = route.screen === props.screen))
}
</script>

View file

@ -0,0 +1,29 @@
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import SidebarManageSection from './SidebarManageSection.vue'
new class extends UnitTestCase {
protected test () {
it('shows all menu items if current user is an admin', () => {
this.beAdmin().render(SidebarManageSection)
screen.getByText('Settings')
screen.getByText('Users')
screen.getByText('Upload')
})
it('shows nothing if current user is not an admin', () => {
this.be().render(SidebarManageSection)
expect(screen.queryByText('Settings')).toBeNull()
expect(screen.queryByText('Upload')).toBeNull()
expect(screen.queryByText('Users')).toBeNull()
})
it('shows only the upload menu item if current user is a Plus user', () => {
this.be().enablePlusEdition().render(SidebarManageSection)
screen.getByText('Upload')
expect(screen.queryByText('Settings')).toBeNull()
expect(screen.queryByText('Users')).toBeNull()
})
}
}

View file

@ -11,7 +11,7 @@
</template>
Settings
</SidebarItem>
<SidebarItem screen="Upload" href="#/upload">
<SidebarItem v-if="allowsUpload" screen="Upload" href="#/upload">
<template #icon>
<Icon :icon="faUpload" fixed-width />
</template>
@ -28,11 +28,12 @@
</template>
<script setup lang="ts">
import { faTools, faUpload, faUsers } from '@fortawesome/free-solid-svg-icons'
import { useAuthorization } from '@/composables'
import { useAuthorization, useUpload } from '@/composables'
import SidebarSection from '@/components/layout/main-wrapper/sidebar/SidebarSection.vue'
import SidebarSectionHeader from '@/components/layout/main-wrapper/sidebar/SidebarSectionHeader.vue'
import SidebarItem from '@/components/layout/main-wrapper/sidebar/SidebarItem.vue'
const { isAdmin } = useAuthorization()
const { allowsUpload } = useUpload()
</script>

View file

@ -0,0 +1,14 @@
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import SidebarToggleButton from './SidebarToggleButton.vue'
new class extends UnitTestCase {
protected test () {
it('emits the toggle event', () => {
const { emitted } = this.render(SidebarToggleButton)
this.trigger(screen.getByRole('checkbox'), 'click')
expect(emitted()['update:modelValue']).toBeTruthy()
})
}
}

View file

@ -0,0 +1,23 @@
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import SidebarYourMusicSection from './SidebarYourMusicSection.vue'
import { eventBus } from '@/utils'
new class extends UnitTestCase {
protected test () {
it('shows YouTube item if a video is played', async () => {
this.render(SidebarYourMusicSection)
expect(screen.queryByTestId('youtube')).toBeNull()
eventBus.emit('PLAY_YOUTUBE_VIDEO', {
id: 'video-id',
title: 'Another One Bites the Dust',
})
await this.tick()
screen.getByTestId('youtube')
screen.getByText('Another One Bites the Dust')
})
}
}

View file

@ -36,16 +36,18 @@
</template>
Genres
</SidebarItem>
<YouTubeSidebarItem v-show="showYouTube" />
<YouTubeSidebarItem v-if="youtubeVideoTitle" data-testid="youtube">
{{ youtubeVideoTitle }}
</YouTubeSidebarItem>
</ul>
</SidebarSection>
</template>
<script setup lang="ts">
import { faCompactDisc, faHome, faMicrophone, faMusic, faTags } from '@fortawesome/free-solid-svg-icons'
import { computed, ref } from 'vue'
import { unescape } from 'lodash'
import { ref } from 'vue'
import { eventBus } from '@/utils'
import { useThirdPartyServices } from '@/composables'
import SidebarSection from '@/components/layout/main-wrapper/sidebar/SidebarSection.vue'
import SidebarSectionHeader from '@/components/layout/main-wrapper/sidebar/SidebarSectionHeader.vue'
@ -53,10 +55,7 @@ import SidebarItem from '@/components/layout/main-wrapper/sidebar/SidebarItem.vu
import QueueSidebarItem from '@/components/layout/main-wrapper/sidebar/QueueSidebarItem.vue'
import YouTubeSidebarItem from '@/components/layout/main-wrapper/sidebar/YouTubeSidebarItem.vue'
const { useYouTube } = useThirdPartyServices()
const youtubeVideoTitle = ref<string | null>(null)
const youTubePlaying = ref(false)
const showYouTube = computed(() => useYouTube.value && youTubePlaying.value)
eventBus.on('PLAY_YOUTUBE_VIDEO', () => (youTubePlaying.value = true))
eventBus.on('PLAY_YOUTUBE_VIDEO', payload => (youtubeVideoTitle.value = unescape(payload.title)))
</script>

View file

@ -1,15 +1,15 @@
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { eventBus } from '@/utils'
import YouTubeSidebarItem from './YouTubeSidebarItem.vue'
new class extends UnitTestCase {
protected test () {
it('renders', async () => {
const { html } = this.render(YouTubeSidebarItem)
eventBus.emit('PLAY_YOUTUBE_VIDEO', { id: '123', title: 'A Random Video' })
await this.tick()
const { html } = this.render(YouTubeSidebarItem, {
slots: {
default: 'Another One Bites the Dust',
},
})
expect(html()).toMatchSnapshot()
})

View file

@ -3,19 +3,12 @@
<template #icon>
<Icon :icon="faYoutube" fixed-width />
</template>
{{ title }}
<slot />
</SidebarItem>
</template>
<script lang="ts" setup>
import { unescape } from 'lodash'
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
import { ref } from 'vue'
import { eventBus } from '@/utils'
import SidebarItem from './SidebarItem.vue'
const title = ref('')
eventBus.on('PLAY_YOUTUBE_VIDEO', payload => (title.value = unescape(payload.title)))
</script>

View file

@ -1,3 +1,3 @@
// Vitest Snapshot v1
exports[`renders 1`] = `<li data-v-7589e0e3="" class="relative before:right-0 px-6 before:top-1/4 before:w-[4px] before:h-1/2 before:absolute before:rounded-full before:transition-[box-shadow,_background-color] before:ease-in-out before:duration-500" data-testid="sidebar-item"><a data-v-7589e0e3="" href="#/youtube" class="flex items-center overflow-x-hidden gap-3 h-11 relative active:pt-0.5 active:pr-0 active:pb-0 active:pl-0.5 !text-k-text-secondary hover:!text-k-text-primary"><span data-v-7589e0e3="" class="opacity-70"><br data-testid="Icon" icon="[object Object]" fixed-width=""></span><span data-v-7589e0e3="" class="overflow-hidden text-ellipsis whitespace-nowrap"> A Random Video</span></a></li>`;
exports[`renders 1`] = `<li data-v-7589e0e3="" class="relative before:right-0 px-6 before:top-1/4 before:w-[4px] before:h-1/2 before:absolute before:rounded-full before:transition-[box-shadow,_background-color] before:ease-in-out before:duration-500" data-testid="sidebar-item"><a data-v-7589e0e3="" href="#/youtube" class="flex items-center overflow-x-hidden gap-3 h-11 relative active:pt-0.5 active:pr-0 active:pb-0 active:pl-0.5 !text-k-text-secondary hover:!text-k-text-primary"><span data-v-7589e0e3="" class="opacity-70"><br data-testid="Icon" icon="[object Object]" fixed-width=""></span><span data-v-7589e0e3="" class="overflow-hidden text-ellipsis whitespace-nowrap">Another One Bites the Dust</span></a></li>`;

View file

@ -0,0 +1,26 @@
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { http } from '@/services'
import CreditsBlock from './CreditsBlock.vue'
new class extends UnitTestCase {
protected test () {
it('renders the credits', async () => {
window.IS_DEMO = true
const getMock = this.mock(http, 'get').mockResolvedValue([
{ name: 'Foo', url: 'https://foo.com' },
{ name: 'Bar', url: 'https://bar.com' },
{ name: 'Something Else', url: 'https://something-else.net' }
])
const { html } = this.render(CreditsBlock)
await this.tick(3)
expect(html()).toMatchSnapshot()
expect(getMock).toHaveBeenCalledWith('demo/credits')
window.IS_DEMO = false
})
}
}

View file

@ -0,0 +1,10 @@
// Vitest Snapshot v1
exports[`renders the credits 1`] = `
<div data-v-7d3b4fcd="" class="max-h-[9rem] overflow-auto" data-testid="demo-credits"> Music by <ul data-v-7d3b4fcd="" class="inline">
<li data-v-7d3b4fcd="" class="inline"><a data-v-7d3b4fcd="" href="https://bar.com" target="_blank">Bar</a></li>
<li data-v-7d3b4fcd="" class="inline"><a data-v-7d3b4fcd="" href="https://foo.com" target="_blank">Foo</a></li>
<li data-v-7d3b4fcd="" class="inline"><a data-v-7d3b4fcd="" href="https://something-else.net" target="_blank">Something Else</a></li>
</ul>
</div>
`;

View file

@ -0,0 +1,20 @@
import { expect, it, vi } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { authService } from '@/services'
import QRLogin from '@/components/profile-preferences/QRLogin.vue'
new class extends UnitTestCase {
protected test () {
it('renders', async () => {
vi.mock('@vueuse/integrations/useQRCode', () => ({
useQRCode: () => 'data:image/png;base64,my-qr-code'
}))
const getTokenMock = this.mock(authService, 'getOneTimeToken').mockResolvedValue('my-token')
const { html } = this.render(QRLogin)
expect(getTokenMock).toHaveBeenCalled()
expect(html()).toMatchSnapshot()
})
}
}

View file

@ -0,0 +1,3 @@
// Vitest Snapshot v1
exports[`renders 1`] = `<article class="text-k-text-secondary"> Instead of using a password, you can scan the QR code below to log in to <a href="https://koel.dev/#mobile" target="_blank" class="text-k-highlight">Koel Player</a> on your mobile device.<br> The QR code will refresh every 10 minutes. <img class="mt-4 rounded-4" src="data:image/png;base64,my-qr-code" alt="QR Code" width="192" height="192"></article>`;

View file

@ -16,7 +16,8 @@
}
},
"include": [
"js/**/*"
"js/**/*",
"../../node_modules/vitest/globals.d.ts"
],
"exclude": [
"js/__tests__/**/*"