feat(test): add AppHeader component tests

This commit is contained in:
Phan An 2022-05-07 10:12:16 +02:00
parent 93c02a6b58
commit 93073814ca
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
9 changed files with 84 additions and 37 deletions

View file

@ -5,7 +5,7 @@ import album from './album'
import song from './song' import song from './song'
import video from './video' import video from './video'
import playlist from './playlist' import playlist from './playlist'
import user from './user' import user, { states as userStates } from './user'
factory factory
.define('artist', (faker: Faker): Artist => artist(faker)) .define('artist', (faker: Faker): Artist => artist(faker))
@ -13,6 +13,6 @@ factory
.define('song', (faker: Faker): Song => song(faker)) .define('song', (faker: Faker): Song => song(faker))
.define('video', (faker: Faker): YouTubeVideo => video(faker)) .define('video', (faker: Faker): YouTubeVideo => video(faker))
.define('playlist', (faker: Faker): Playlist => playlist(faker)) .define('playlist', (faker: Faker): Playlist => playlist(faker))
.define('user', (faker: Faker): User => user(faker)) .define('user', (faker: Faker): User => user(faker), userStates)
export default factory export default factory

View file

@ -9,3 +9,9 @@ export default (faker: Faker): User => ({
avatar: 'https://gravatar.com/foo', avatar: 'https://gravatar.com/foo',
preferences: {} preferences: {}
}) })
export const states = {
admin: {
is_admin: true
}
}

View file

@ -0,0 +1,61 @@
import { beforeEach, expect, it } from 'vitest'
import { mockHelper, render } from '@/__tests__/__helpers__'
import { cleanup, fireEvent, queryAllByTestId } from '@testing-library/vue'
import { eventBus } from '@/utils'
import { nextTick } from 'vue'
import isMobile from 'ismobilejs'
import AppHeader from './AppHeader.vue'
import SearchForm from '@/components/ui/SearchForm.vue'
import compareVersions from 'compare-versions'
import { userStore } from '@/stores'
import factory from '@/__tests__/factory'
beforeEach(() => {
cleanup()
mockHelper.restoreAllMocks()
isMobile.any = false
})
it('toggles sidebar (mobile only)', async () => {
isMobile.any = true
const { getByTitle } = render(AppHeader)
const mock = mockHelper.mock(eventBus, 'emit')
await fireEvent.click(getByTitle('Show or hide the sidebar'))
expect(mock).toHaveBeenCalledWith('TOGGLE_SIDEBAR')
})
it('toggles search form (mobile only)', async () => {
isMobile.any = true
const { getByTitle, getByTestId, queryByTestId } = render(AppHeader, {
global: {
stubs: {
SearchForm
}
}
})
expect(await queryByTestId('search-form')).toBe(null)
await fireEvent.click(getByTitle('Show or hide the search form'))
await nextTick()
getByTestId('search-form')
})
it.each([[true, true, true], [false, true, false], [true, false, false], [false, false, false]])(
'announces a new version if applicable',
async (hasNewVersion, isAdmin, announcing) => {
mockHelper.mock(compareVersions, 'compare', hasNewVersion)
userStore.state.current = factory<User>('user', {
is_admin: isAdmin
})
const { queryAllByTestId } = render(AppHeader)
expect(await queryAllByTestId('new-version')).toHaveLength(announcing ? 1 : 0)
}
)

View file

@ -1,15 +1,15 @@
<template> <template>
<header id="mainHeader"> <header id="mainHeader">
<h1 class="brand" v-once>{{ appConfig.name }}</h1> <h1 class="brand">Koel</h1>
<span class="hamburger" role="button" title="Show or hide the sidebar" @click="toggleSidebar"> <span class="hamburger" role="button" title="Show or hide the sidebar" @click="toggleSidebar">
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</span> </span>
<span class="magnifier" role="button" title="Show or hide the search form" @click="toggleSearchForm"> <span class="magnifier" role="button" title="Show or hide the search form" @click="toggleSearchForm">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</span> </span>
<search-form/> <SearchForm v-if="showSearchForm"/>
<div class="header-right"> <div class="header-right">
<user-badge/> <UserBadge/>
<button <button
class="about control" class="about control"
data-testid="about-btn" data-testid="about-btn"
@ -17,33 +17,29 @@
type="button" type="button"
@click.prevent="showAboutDialog" @click.prevent="showAboutDialog"
> >
<span v-if="shouldNotifyNewVersion" class="new-version" data-test="new-version-available"> <span v-if="shouldNotifyNewVersion" class="new-version" data-testid="new-version">
{{ latestVersion }} available! {{ latestVersion }} available!
</span> </span>
<i v-else class="fa fa-info-circle"></i> <i v-else class="fa fa-info-circle"></i>
</button> </button>
</div> </div>
</header> </header>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, toRef } from 'vue' import { defineAsyncComponent, ref } from 'vue'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import { app as appConfig } from '@/config'
import { commonStore, userStore } from '@/stores'
import { useNewVersionNotification } from '@/composables' import { useNewVersionNotification } from '@/composables'
import isMobile from 'ismobilejs'
const SearchForm = defineAsyncComponent(() => import('@/components/ui/SearchForm.vue')) const SearchForm = defineAsyncComponent(() => import('@/components/ui/SearchForm.vue'))
const UserBadge = defineAsyncComponent(() => import('@/components/user/UserBadge.vue')) const UserBadge = defineAsyncComponent(() => import('@/components/user/UserBadge.vue'))
const user = toRef(userStore.state, 'current') const showSearchForm = ref(!isMobile.any)
const state = commonStore.state
const { shouldNotifyNewVersion, latestVersion } = useNewVersionNotification() const { shouldNotifyNewVersion, latestVersion } = useNewVersionNotification()
const toggleSidebar = () => eventBus.emit('TOGGLE_SIDEBAR') const toggleSidebar = () => eventBus.emit('TOGGLE_SIDEBAR')
const toggleSearchForm = () => eventBus.emit('TOGGLE_SEARCH_FORM') const toggleSearchForm = () => (showSearchForm.value = !showSearchForm.value)
const showAboutDialog = () => eventBus.emit('MODAL_SHOW_ABOUT_KOEL') const showAboutDialog = () => eventBus.emit('MODAL_SHOW_ABOUT_KOEL')
</script> </script>
@ -89,7 +85,7 @@ const showAboutDialog = () => eventBus.emit('MODAL_SHOW_ABOUT_KOEL')
@media only screen and (max-width: 667px) { @media only screen and (max-width: 667px) {
display: flex; display: flex;
align-content: stretch; align-content: stretch;
justify-content: flext-start; justify-content: flex-start;
.hamburger, .magnifier { .hamburger, .magnifier {
display: inline-block; display: inline-block;

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="side search" id="searchForm" :class="{ showing }" role="search"> <div id="searchForm" class="side search" data-testid="search-form" role="search">
<input <input
ref="input" ref="input"
v-model="q" v-model="q"
@ -16,16 +16,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import isMobile from 'ismobilejs'
import { ref } from 'vue' import { ref } from 'vue'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import router from '@/router' import router from '@/router'
const input = ref<HTMLInputElement>() const input = ref<HTMLInputElement>()
const q = ref('') const q = ref('')
const showing = ref(!isMobile.phone)
const onInput = debounce(() => { const onInput = debounce(() => {
const _q = q.value.trim() const _q = q.value.trim()
@ -35,8 +32,6 @@ const onInput = debounce(() => {
const goToSearchScreen = () => router.go('/search') const goToSearchScreen = () => router.go('/search')
eventBus.on({ eventBus.on({
'TOGGLE_SEARCH_FORM': () => (showing.value = !showing.value),
FOCUS_SEARCH_FIELD () { FOCUS_SEARCH_FIELD () {
input.value?.focus() input.value?.focus()
input.value?.select() input.value?.select()
@ -56,19 +51,14 @@ eventBus.on({
} }
@media only screen and (max-width: 667px) { @media only screen and (max-width: 667px) {
z-index: -1; z-index: 100;
position: absolute; position: absolute;
left: 0; left: 0;
background: var(--color-bg-primary); background: var(--color-bg-primary);
width: 100%; width: 100%;
padding: 12px; padding: 12px;
top: 0; top: var(--header-height);
border-bottom: 1px solid rgba(255, 255, 255, .1);
&.showing {
top: var(--header-height);
border-bottom: 1px solid rgba(255, 255, 255, .1);
z-index: 100;
}
input[type="search"] { input[type="search"] {
width: 100%; width: 100%;

View file

@ -1,3 +0,0 @@
export const app = {
name: 'Koel'
}

View file

@ -4,7 +4,6 @@ export type EventName =
| 'LOAD_MAIN_CONTENT' | 'LOAD_MAIN_CONTENT'
| 'LOG_OUT' | 'LOG_OUT'
| 'TOGGLE_SIDEBAR' | 'TOGGLE_SIDEBAR'
| 'TOGGLE_SEARCH_FORM'
| 'SHOW_OVERLAY' | 'SHOW_OVERLAY'
| 'HIDE_OVERLAY' | 'HIDE_OVERLAY'
| 'FOCUS_SEARCH_FIELD' | 'FOCUS_SEARCH_FIELD'

View file

@ -1,4 +1,3 @@
export * from './app'
export * from './events' export * from './events'
export * from './upload.types' export * from './upload.types'
export * from './acceptedMediaTypes' export * from './acceptedMediaTypes'

View file

@ -15,7 +15,6 @@ import {
} from '@/stores' } from '@/stores'
import { audioService, socketService } from '@/services' import { audioService, socketService } from '@/services'
import { app } from '@/config'
import router from '@/router' import router from '@/router'
/** /**
@ -156,7 +155,7 @@ export const playbackService = {
return return
} }
document.title = `${song.title}${app.name}` document.title = `${song.title}Koel`
this.player!.media.setAttribute('title', `${song.artist.name} - ${song.title}`) this.player!.media.setAttribute('title', `${song.artist.name} - ${song.title}`)
if (queueStore.current) { if (queueStore.current) {
@ -332,7 +331,7 @@ export const playbackService = {
}, },
stop () { stop () {
document.title = app.name document.title = 'Koel'
this.getPlayer().pause() this.getPlayer().pause()
this.getPlayer().seek(0) this.getPlayer().seek(0)