mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
chore: reformat code
This commit is contained in:
parent
8f1aebb357
commit
43795e6ffd
199 changed files with 1257 additions and 1290 deletions
|
@ -30,7 +30,7 @@
|
|||
<AcceptInvitation v-if="layout === 'invitation'" />
|
||||
<ResetPasswordForm v-if="layout === 'reset-password'" />
|
||||
|
||||
<AppInitializer v-if="authenticated" @success="onInitSuccess" @error="onInitError" />
|
||||
<AppInitializer v-if="authenticated" @error="onInitError" @success="onInitSuccess" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -151,7 +151,7 @@ provide(CurrentSongKey, currentSong)
|
|||
|
||||
<style lang="postcss">
|
||||
#dragGhost {
|
||||
@apply inline-block py-2 px-3 rounded-md text-base font-sans fixed top-0 left-0 z-[-1] bg-k-success
|
||||
@apply inline-block py-2 pl-8 pr-3 rounded-md text-base font-sans fixed top-0 left-0 z-[-1] bg-k-success
|
||||
text-k-text-primary no-hover:hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,10 +27,18 @@ const deepMerge = (first: object, second: object) => {
|
|||
})
|
||||
}
|
||||
|
||||
const setPropIfNotExists = (obj: object | null, prop: any, value: any) => {
|
||||
if (!obj) return
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
||||
obj[prop] = value
|
||||
}
|
||||
}
|
||||
|
||||
export default abstract class UnitTestCase {
|
||||
private backupMethods = new Map()
|
||||
protected router: Router
|
||||
protected user: UserEvent
|
||||
private backupMethods = new Map()
|
||||
|
||||
public constructor () {
|
||||
this.router = new Router(routes)
|
||||
|
@ -108,37 +116,6 @@ export default abstract class UnitTestCase {
|
|||
}, this.supplyRequiredProvides(options)))
|
||||
}
|
||||
|
||||
private supplyRequiredProvides (options: RenderOptions) {
|
||||
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
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!options.global.provide.hasOwnProperty(OverlayKey)) {
|
||||
// @ts-ignore
|
||||
options.global.provide[OverlayKey] = OverlayStub
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!options.global.provide.hasOwnProperty(RouterKey)) {
|
||||
// @ts-ignore
|
||||
options.global.provide[RouterKey] = this.router
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
protected enablePlusEdition () {
|
||||
commonStore.state.koel_plus = {
|
||||
active: true,
|
||||
|
@ -189,9 +166,21 @@ export default abstract class UnitTestCase {
|
|||
await this.user.type(element, value)
|
||||
}
|
||||
|
||||
protected async trigger(element: HTMLElement, key: EventType | string, options?: {}) {
|
||||
protected async trigger (element: HTMLElement, key: EventType | string, options?: {}) {
|
||||
await fireEvent(element, createEvent[key](element, options))
|
||||
}
|
||||
|
||||
protected abstract test ()
|
||||
|
||||
private supplyRequiredProvides (options: RenderOptions) {
|
||||
options.global = options.global || {}
|
||||
options.global.provide = options.global.provide || {}
|
||||
|
||||
setPropIfNotExists(options.global.provide, DialogBoxKey, DialogBoxStub)
|
||||
setPropIfNotExists(options.global.provide, MessageToasterKey, MessageToasterStub)
|
||||
setPropIfNotExists(options.global.provide, OverlayKey, OverlayStub)
|
||||
setPropIfNotExists(options.global.provide, RouterKey, this.router)
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,26 +10,6 @@ import AlbumCard from './AlbumCard.vue'
|
|||
let album: Album
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
album = factory<Album>('album', {
|
||||
id: 42,
|
||||
name: 'IV',
|
||||
artist_id: 17,
|
||||
artist_name: 'Led Zeppelin'
|
||||
})
|
||||
|
||||
return this.render(AlbumCard, {
|
||||
props: {
|
||||
album
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
|
@ -70,4 +50,24 @@ new class extends UnitTestCase {
|
|||
expect(emitMock).toHaveBeenCalledWith('ALBUM_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), album)
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
album = factory<Album>('album', {
|
||||
id: 42,
|
||||
name: 'IV',
|
||||
artist_id: 17,
|
||||
artist_name: 'Led Zeppelin'
|
||||
})
|
||||
|
||||
return this.render(AlbumCard, {
|
||||
props: {
|
||||
album
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<ArtistAlbumCard
|
||||
v-if="showing"
|
||||
:entity="album"
|
||||
:title="`${album.name} by ${album.artist_name}`"
|
||||
:layout="layout"
|
||||
:title="`${album.name} by ${album.artist_name}`"
|
||||
@contextmenu="requestContextMenu"
|
||||
@dblclick="shuffle"
|
||||
@dragstart="onDragStart"
|
||||
|
|
|
@ -11,18 +11,6 @@ import AlbumContextMenu from './AlbumContextMenu.vue'
|
|||
let album: Album
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (_album?: Album) {
|
||||
album = _album || factory<Album>('album', {
|
||||
name: 'IV'
|
||||
})
|
||||
|
||||
const rendered = this.render(AlbumContextMenu)
|
||||
eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, album)
|
||||
await this.tick(2)
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => expect((await this.renderComponent()).html()).toMatchSnapshot())
|
||||
|
||||
|
@ -94,4 +82,16 @@ new class extends UnitTestCase {
|
|||
expect(mock).toHaveBeenCalledWith(`artist/${album.artist_id}`)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (_album?: Album) {
|
||||
album = _album || factory<Album>('album', {
|
||||
name: 'IV'
|
||||
})
|
||||
|
||||
const rendered = this.render(AlbumContextMenu)
|
||||
eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, album)
|
||||
await this.tick(2)
|
||||
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,22 @@ import AlbumInfoComponent from './AlbumInfo.vue'
|
|||
let album: Album
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
||||
await this.renderComponent(mode)
|
||||
|
||||
screen.getByTestId('album-info-tracks')
|
||||
|
||||
if (mode === 'aside') {
|
||||
screen.getByTestId('thumbnail')
|
||||
} else {
|
||||
expect(screen.queryByTestId('thumbnail')).toBeNull()
|
||||
}
|
||||
|
||||
expect(screen.getByTestId('album-info').classList.contains(mode)).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (mode: MediaInfoDisplayMode = 'aside', info?: AlbumInfo) {
|
||||
commonStore.state.uses_last_fm = true
|
||||
|
||||
|
@ -37,20 +53,4 @@ new class extends UnitTestCase {
|
|||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
||||
await this.renderComponent(mode)
|
||||
|
||||
screen.getByTestId('album-info-tracks')
|
||||
|
||||
if (mode === 'aside') {
|
||||
screen.getByTestId('thumbnail')
|
||||
} else {
|
||||
expect(screen.queryByTestId('thumbnail')).toBeNull()
|
||||
}
|
||||
|
||||
expect(screen.getByTestId('album-info').classList.contains(mode)).toBe(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
v-if="info.tracks?.length"
|
||||
:album="album"
|
||||
:tracks="info.tracks"
|
||||
data-testid="album-info-tracks"
|
||||
class="mt-8"
|
||||
data-testid="album-info-tracks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<li
|
||||
v-for="(track, index) in tracks"
|
||||
:key="index"
|
||||
data-testid="album-track-item"
|
||||
class="flex p-2 before:w-7 before:opacity-50"
|
||||
data-testid="album-track-item"
|
||||
>
|
||||
<TrackListItem :album="album" :track="track" />
|
||||
</li>
|
||||
|
|
|
@ -9,6 +9,23 @@ import { ref } from 'vue'
|
|||
import AlbumTrackListItem from './AlbumTrackListItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
it('plays', async () => {
|
||||
const matchedSong = factory<Song>('song')
|
||||
const queueMock = this.mock(queueStore, 'queueIfNotQueued')
|
||||
const playMock = this.mock(playbackService, 'play')
|
||||
|
||||
this.renderComponent(matchedSong)
|
||||
|
||||
await this.user.click(screen.getByTitle('Click to play'))
|
||||
|
||||
expect(queueMock).toHaveBeenNthCalledWith(1, matchedSong)
|
||||
expect(playMock).toHaveBeenNthCalledWith(1, matchedSong)
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (matchedSong?: Song) {
|
||||
const songsToMatchAgainst = factory<Song>('song', 10)
|
||||
const album = factory<Album>('album')
|
||||
|
@ -36,21 +53,4 @@ new class extends UnitTestCase {
|
|||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
it('plays', async () => {
|
||||
const matchedSong = factory<Song>('song')
|
||||
const queueMock = this.mock(queueStore, 'queueIfNotQueued')
|
||||
const playMock = this.mock(playbackService, 'play')
|
||||
|
||||
this.renderComponent(matchedSong)
|
||||
|
||||
await this.user.click(screen.getByTitle('Click to play'))
|
||||
|
||||
expect(queueMock).toHaveBeenNthCalledWith(1, matchedSong)
|
||||
expect(playMock).toHaveBeenNthCalledWith(1, matchedSong)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
class="track-list-item flex flex-1 gap-1"
|
||||
:class="{ active, available: matchedSong }"
|
||||
:title="tooltip"
|
||||
class="track-list-item flex flex-1 gap-1"
|
||||
tabindex="0"
|
||||
@click="play"
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<article data-v-f01bdc56="" class="relative flex max-w-full md:max-w-[256px] border p-5 rounded-lg flex-col gap-5 transition border-color duration-200 full" draggable="true" tabindex="0" data-testid="artist-album-card" title="IV by Led Zeppelin">
|
||||
<article data-v-f01bdc56="" class="full relative flex max-w-full md:max-w-[256px] border p-5 rounded-lg flex-col gap-5 transition border-color duration-200" data-testid="artist-album-card" draggable="true" tabindex="0" title="IV by Led Zeppelin">
|
||||
<div data-v-a14c1d10="" data-v-f01bdc56="" class="cover relative w-full aspect-square bg-no-repeat bg-cover bg-center overflow-hidden rounded-md after:block after:pt-[100%]" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail"><img data-v-a14c1d10="" alt="IV" src="http://loremflickr.com/640/480" class="w-full h-full object-cover absolute left-0 top-0 pointer-events-none before:absolute before:w-full before:h-full before:opacity-0 before:z-[1] before-top-0" loading="lazy"><a data-v-a14c1d10="" class="control control-play h-full w-full absolute flex justify-center items-center" role="button"><span data-v-a14c1d10="" class="hidden">Play all songs in the album IV</span><span data-v-a14c1d10="" class="icon opacity-0 w-1/2 h-1/2 flex justify-center items-center pointer-events-none pl-[4%] rounded-full after:w-full after:h-full"></span></a></div>
|
||||
<footer data-v-f01bdc56="" class="flex flex-1 flex-col gap-1.5 overflow-hidden">
|
||||
<div data-v-f01bdc56="" class="name flex flex-col gap-2 whitespace-nowrap"><a href="#/album/42" class="font-medium" data-testid="name">IV</a><a href="#/artist/17">Led Zeppelin</a></div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<nav data-v-0408531a="" class="album-menu menu context-menu select-none" style="top: 42px; left: 420px;" tabindex="0" data-testid="album-context-menu">
|
||||
<nav data-v-0408531a="" class="album-menu menu context-menu select-none" tabindex="0" data-testid="album-context-menu">
|
||||
<ul data-v-0408531a="">
|
||||
<li>Play All</li>
|
||||
<li>Shuffle All</li>
|
||||
|
|
|
@ -19,19 +19,6 @@ new class extends UnitTestCase {
|
|||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(ArtistCard, {
|
||||
props: {
|
||||
artist
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
|
@ -72,4 +59,17 @@ new class extends UnitTestCase {
|
|||
expect(emitMock).toHaveBeenCalledWith('ARTIST_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), artist)
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(ArtistCard, {
|
||||
props: {
|
||||
artist
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,18 +11,6 @@ import ArtistContextMenu from './ArtistContextMenu.vue'
|
|||
let artist: Artist
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (_artist?: Artist) {
|
||||
artist = _artist || factory<Artist>('artist', {
|
||||
name: 'Accept'
|
||||
})
|
||||
|
||||
const rendered = this.render(ArtistContextMenu)
|
||||
eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, artist)
|
||||
await this.tick(2)
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => expect((await this.renderComponent()).html()).toMatchSnapshot())
|
||||
|
||||
|
@ -91,4 +79,16 @@ new class extends UnitTestCase {
|
|||
expect(screen.queryByText('Download')).toBeNull()
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (_artist?: Artist) {
|
||||
artist = _artist || factory<Artist>('artist', {
|
||||
name: 'Accept'
|
||||
})
|
||||
|
||||
const rendered = this.render(ArtistContextMenu)
|
||||
eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, artist)
|
||||
await this.tick(2)
|
||||
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,20 @@ import ArtistInfoComponent from './ArtistInfo.vue'
|
|||
let artist: Artist
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
||||
await this.renderComponent(mode)
|
||||
|
||||
if (mode === 'aside') {
|
||||
screen.getByTestId('thumbnail')
|
||||
} else {
|
||||
expect(screen.queryByTestId('thumbnail')).toBeNull()
|
||||
}
|
||||
|
||||
expect(screen.getByTestId('artist-info').classList.contains(mode)).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (mode: MediaInfoDisplayMode = 'aside', info?: ArtistInfo) {
|
||||
commonStore.state.uses_last_fm = true
|
||||
info = info ?? factory<ArtistInfo>('artist-info')
|
||||
|
@ -33,18 +47,4 @@ new class extends UnitTestCase {
|
|||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
||||
await this.renderComponent(mode)
|
||||
|
||||
if (mode === 'aside') {
|
||||
screen.getByTestId('thumbnail')
|
||||
} else {
|
||||
expect(screen.queryByTestId('thumbnail')).toBeNull()
|
||||
}
|
||||
|
||||
expect(screen.getByTestId('artist-info').classList.contains(mode)).toBe(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<article data-v-f01bdc56="" class="relative flex max-w-full md:max-w-[256px] border p-5 rounded-lg flex-col gap-5 transition border-color duration-200 full" draggable="true" tabindex="0" data-testid="artist-album-card" title="Led Zeppelin">
|
||||
<article data-v-f01bdc56="" class="full relative flex max-w-full md:max-w-[256px] border p-5 rounded-lg flex-col gap-5 transition border-color duration-200" data-testid="artist-album-card" draggable="true" tabindex="0" title="Led Zeppelin">
|
||||
<div data-v-a14c1d10="" data-v-f01bdc56="" class="cover relative w-full aspect-square bg-no-repeat bg-cover bg-center overflow-hidden rounded-md after:block after:pt-[100%]" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail"><img data-v-a14c1d10="" alt="Led Zeppelin" src="foo.jpg" class="w-full h-full object-cover absolute left-0 top-0 pointer-events-none before:absolute before:w-full before:h-full before:opacity-0 before:z-[1] before-top-0" loading="lazy"><a data-v-a14c1d10="" class="control control-play h-full w-full absolute flex justify-center items-center" role="button"><span data-v-a14c1d10="" class="hidden">Play all songs by Led Zeppelin</span><span data-v-a14c1d10="" class="icon opacity-0 w-1/2 h-1/2 flex justify-center items-center pointer-events-none pl-[4%] rounded-full after:w-full after:h-full"></span></a></div>
|
||||
<footer data-v-f01bdc56="" class="flex flex-1 flex-col gap-1.5 overflow-hidden">
|
||||
<div data-v-f01bdc56="" class="name flex flex-col gap-2 whitespace-nowrap"><a href="#/artist/42" class="font-medium" data-testid="name">Led Zeppelin</a></div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `
|
||||
<nav data-v-0408531a="" class="artist-menu menu context-menu select-none" style="top: 42px; left: 420px;" tabindex="0" data-testid="artist-context-menu">
|
||||
<nav data-v-0408531a="" class="artist-menu menu context-menu select-none" tabindex="0" data-testid="artist-context-menu">
|
||||
<ul data-v-0408531a="">
|
||||
<li>Play All</li>
|
||||
<li>Shuffle All</li>
|
||||
|
|
|
@ -10,18 +10,18 @@
|
|||
<div class="flex flex-col gap-3 sm:flex-row sm:gap-0 sm:content-stretch">
|
||||
<TextInput
|
||||
v-model="email"
|
||||
placeholder="Your email address"
|
||||
required type="email"
|
||||
class="flex-1 sm:rounded-l sm:rounded-r-none"
|
||||
placeholder="Your email address" required
|
||||
type="email"
|
||||
/>
|
||||
<Btn :disabled="loading" type="submit" class="sm:rounded-l-none sm:rounded-r">Reset Password</Btn>
|
||||
<Btn :disabled="loading" class="sm:rounded-l-none sm:rounded-r" type="submit">Reset Password</Btn>
|
||||
<Btn :disabled="loading" class="!text-k-text-secondary" transparent @click="cancel">Cancel</Btn>
|
||||
</div>
|
||||
</FormRow>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { authService } from '@/services'
|
||||
import { useErrorHandler, useMessageToaster } from '@/composables'
|
||||
|
|
|
@ -6,18 +6,6 @@ import { logger } from '@/utils'
|
|||
import LoginFrom from './LoginForm.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async submitForm (loginMock: Mock) {
|
||||
const rendered = this.render(LoginFrom)
|
||||
|
||||
await this.type(screen.getByPlaceholderText('Email Address'), 'john@doe.com')
|
||||
await this.type(screen.getByPlaceholderText('Password'), 'secret')
|
||||
await this.user.click(screen.getByTestId('submit'))
|
||||
|
||||
expect(loginMock).toHaveBeenCalledWith('john@doe.com', 'secret')
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.render(LoginFrom).html()).toMatchSnapshot())
|
||||
|
||||
|
@ -67,4 +55,16 @@ new class extends UnitTestCase {
|
|||
window.SSO_PROVIDERS = []
|
||||
})
|
||||
}
|
||||
|
||||
private async submitForm (loginMock: Mock) {
|
||||
const rendered = this.render(LoginFrom)
|
||||
|
||||
await this.type(screen.getByPlaceholderText('Email Address'), 'john@doe.com')
|
||||
await this.type(screen.getByPlaceholderText('Password'), 'secret')
|
||||
await this.user.click(screen.getByTestId('submit'))
|
||||
|
||||
expect(loginMock).toHaveBeenCalledWith('john@doe.com', 'secret')
|
||||
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
<div class="flex items-center justify-center min-h-screen my-0 mx-auto flex-col gap-5">
|
||||
<form
|
||||
v-show="!showingForgotPasswordForm"
|
||||
class="w-full sm:w-[288px] sm:border duration-500 p-7 rounded-xl border-transparent sm:bg-white/10 space-y-3"
|
||||
:class="{ error: failed }"
|
||||
class="w-full sm:w-[288px] sm:border duration-500 p-7 rounded-xl border-transparent sm:bg-white/10 space-y-3"
|
||||
data-testid="login-form"
|
||||
@submit.prevent="login"
|
||||
>
|
||||
<div class="text-center mb-8">
|
||||
<img class="inline-block" alt="Koel's logo" src="@/../img/logo.svg" width="156">
|
||||
<img alt="Koel's logo" class="inline-block" src="@/../img/logo.svg" width="156">
|
||||
</div>
|
||||
|
||||
<FormRow>
|
||||
|
@ -20,7 +20,7 @@
|
|||
</FormRow>
|
||||
|
||||
<FormRow>
|
||||
<Btn type="submit" data-testid="submit">Log In</Btn>
|
||||
<Btn data-testid="submit" type="submit">Log In</Btn>
|
||||
</FormRow>
|
||||
|
||||
<FormRow v-if="canResetPassword">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { authService } from '@/services'
|
||||
import { base64Decode } from '@/utils'
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
exports[`renders 1`] = `
|
||||
<div data-v-0b0f87ea="" class="flex items-center justify-center min-h-screen my-0 mx-auto flex-col gap-5">
|
||||
<form data-v-0b0f87ea="" class="w-full sm:w-[288px] sm:border duration-500 p-7 rounded-xl border-transparent sm:bg-white/10 space-y-3" data-testid="login-form">
|
||||
<div data-v-0b0f87ea="" class="text-center mb-8"><img data-v-0b0f87ea="" class="inline-block" alt="Koel's logo" src="undefined/resources/assets/img/logo.svg" width="156"></div><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<div data-v-0b0f87ea="" class="text-center mb-8"><img data-v-0b0f87ea="" alt="Koel's logo" class="inline-block" src="undefined/resources/assets/img/logo.svg" width="156"></div><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if--><input data-v-0b0f87ea="" class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900" type="email" autofocus="" placeholder="Email Address" required="">
|
||||
<!--v-if-->
|
||||
</label><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if-->
|
||||
<div data-v-0b0f87ea="" class="relative"><input class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900 w-full" type="password" data-testid="input" placeholder="Password" required=""><button class="absolute p-2.5 right-0 top-0 text-k-bg-primary" type="button" data-testid="toggle"><br data-testid="Icon" icon="[object Object]"></button></div>
|
||||
<div data-v-0b0f87ea="" class="relative"><input class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900 w-full" type="password" data-testid="input" placeholder="Password" required=""><button class="absolute p-2.5 right-0 top-0 text-k-bg-primary" data-testid="toggle" type="button"><br data-testid="Icon" icon="[object Object]"></button></div>
|
||||
<!--v-if-->
|
||||
</label><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if--><button data-v-8943c846="" data-v-0b0f87ea="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer" type="submit" data-testid="submit">Log In</button>
|
||||
|
@ -26,12 +26,12 @@ exports[`renders 1`] = `
|
|||
exports[`shows Google login button 1`] = `
|
||||
<div data-v-0b0f87ea="" class="flex items-center justify-center min-h-screen my-0 mx-auto flex-col gap-5">
|
||||
<form data-v-0b0f87ea="" class="w-full sm:w-[288px] sm:border duration-500 p-7 rounded-xl border-transparent sm:bg-white/10 space-y-3" data-testid="login-form">
|
||||
<div data-v-0b0f87ea="" class="text-center mb-8"><img data-v-0b0f87ea="" class="inline-block" alt="Koel's logo" src="undefined/resources/assets/img/logo.svg" width="156"></div><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<div data-v-0b0f87ea="" class="text-center mb-8"><img data-v-0b0f87ea="" alt="Koel's logo" class="inline-block" src="undefined/resources/assets/img/logo.svg" width="156"></div><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if--><input data-v-0b0f87ea="" class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900" type="email" autofocus="" placeholder="Email Address" required="">
|
||||
<!--v-if-->
|
||||
</label><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if-->
|
||||
<div data-v-0b0f87ea="" class="relative"><input class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900 w-full" type="password" data-testid="input" placeholder="Password" required=""><button class="absolute p-2.5 right-0 top-0 text-k-bg-primary" type="button" data-testid="toggle"><br data-testid="Icon" icon="[object Object]"></button></div>
|
||||
<div data-v-0b0f87ea="" class="relative"><input class="block text-base w-full px-4 py-2.5 rounded bg-k-bg-input text-k-text-input read-only:bg-gray-400 read-only:text-gray-900 disabled:bg-gray-400 disabled:text-gray-900 w-full" type="password" data-testid="input" placeholder="Password" required=""><button class="absolute p-2.5 right-0 top-0 text-k-bg-primary" data-testid="toggle" type="button"><br data-testid="Icon" icon="[object Object]"></button></div>
|
||||
<!--v-if-->
|
||||
</label><label data-v-0b0f87ea="" class="flex flex-col gap-2 text-[1.1rem]">
|
||||
<!--v-if--><button data-v-8943c846="" data-v-0b0f87ea="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer" type="submit" data-testid="submit">Log In</button>
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
type="button"
|
||||
@click.prevent="loginWithGoogle"
|
||||
>
|
||||
<img :src="googleLogo" alt="Google Logo" width="32" height="32">
|
||||
<img :src="googleLogo" alt="Google Logo" height="32" width="32">
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import googleLogo from '@/../img/logos/google.svg'
|
||||
import { openPopup } from '@/utils'
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { invitationService } from '@/services'
|
||||
import { useErrorHandler, useRouter } from '@/composables'
|
||||
|
|
|
@ -17,9 +17,9 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: '_',
|
||||
screen: 'Invitation.Accept'
|
||||
}, {
|
||||
path: '_',
|
||||
screen: 'Invitation.Accept'
|
||||
}, {
|
||||
token: 'my-token'
|
||||
})
|
||||
|
||||
|
|
|
@ -5,10 +5,6 @@ import { plusService } from '@/services'
|
|||
import Form from './ActivateLicenseForm.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(Form )
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('activates license', async () => {
|
||||
this.renderComponent()
|
||||
|
@ -19,4 +15,8 @@ new class extends UnitTestCase {
|
|||
expect(activateMock).toHaveBeenCalledWith('my-license-key')
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(Form)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<Btn :disabled="loading" class="!rounded-l-none" type="submit">Activate</Btn>
|
||||
</form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { plusService } from '@/services'
|
||||
import { forceReloadWindow } from '@/utils'
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</Btn>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
|
|
|
@ -5,10 +5,6 @@ import UnitTestCase from '@/__tests__/UnitTestCase'
|
|||
import Modal from './KoelPlusModal.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(Modal)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('shows button to purchase Koel Plus', async () => {
|
||||
commonStore.state.koel_plus.product_id = '42'
|
||||
|
@ -29,4 +25,8 @@ new class extends UnitTestCase {
|
|||
screen.getByTestId('activateForm')
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(Modal)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="plus text-k-text-secondary max-w-[480px] flex flex-col items-center" data-testid="koel-plus" tabindex="0">
|
||||
<img
|
||||
class="-mt-[48px] rounded-full border-[6px] border-white"
|
||||
alt="Koel Plus"
|
||||
class="-mt-[48px] rounded-full border-[6px] border-white"
|
||||
src="@/../img/koel-plus.svg"
|
||||
width="96"
|
||||
>
|
||||
|
@ -30,12 +30,12 @@
|
|||
</main>
|
||||
|
||||
<footer class="w-full text-center bg-black/20">
|
||||
<Btn data-testid="close-modal-btn" danger rounded @click.prevent="close">Close</Btn>
|
||||
<Btn danger data-testid="close-modal-btn" rounded @click.prevent="close">Close</Btn>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useKoelPlus } from '@/composables'
|
||||
|
||||
|
|
|
@ -5,17 +5,6 @@ import { eventBus } from '@/utils'
|
|||
import FooterExtraControls from './FooterExtraControls.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(FooterExtraControls, {
|
||||
global: {
|
||||
stubs: {
|
||||
Equalizer: this.stub('Equalizer'),
|
||||
Volume: this.stub('Volume')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => {
|
||||
this.setReadOnlyProperty(document, 'fullscreenEnabled', undefined)
|
||||
|
@ -32,4 +21,15 @@ new class extends UnitTestCase {
|
|||
expect(emitMock).toHaveBeenCalledWith('FULLSCREEN_TOGGLE')
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(FooterExtraControls, {
|
||||
global: {
|
||||
stubs: {
|
||||
Equalizer: this.stub('Equalizer'),
|
||||
Volume: this.stub('Volume')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,29 @@ import { screen } from '@testing-library/vue'
|
|||
import FooterPlaybackControls from './FooterPlaybackControls.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('renders without a current song', () => expect(this.renderComponent(null).html()).toMatchSnapshot())
|
||||
it('renders with a current song', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
it('plays the previous song', async () => {
|
||||
const playMock = this.mock(playbackService, 'playPrev')
|
||||
this.renderComponent()
|
||||
|
||||
await this.user.click(screen.getByRole('button', { name: 'Play previous song' }))
|
||||
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('plays the next song', async () => {
|
||||
const playMock = this.mock(playbackService, 'playNext')
|
||||
this.renderComponent()
|
||||
|
||||
await this.user.click(screen.getByRole('button', { name: 'Play next song' }))
|
||||
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (song?: Song | null) {
|
||||
if (song === undefined) {
|
||||
song = factory<Song>('song', {
|
||||
|
@ -32,27 +55,4 @@ new class extends UnitTestCase {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders without a current song', () => expect(this.renderComponent(null).html()).toMatchSnapshot())
|
||||
it('renders with a current song', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
it('plays the previous song', async () => {
|
||||
const playMock = this.mock(playbackService, 'playPrev')
|
||||
this.renderComponent()
|
||||
|
||||
await this.user.click(screen.getByRole('button', { name: 'Play previous song' }))
|
||||
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('plays the next song', async () => {
|
||||
const playMock = this.mock(playbackService, 'playNext')
|
||||
this.renderComponent()
|
||||
|
||||
await this.user.click(screen.getByRole('button', { name: 'Play next song' }))
|
||||
|
||||
expect(playMock).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
<div v-if="song" class="meta overflow-hidden hidden md:block">
|
||||
<h3 class="title text-ellipsis overflow-hidden whitespace-nowrap">{{ song.title }}</h3>
|
||||
<a
|
||||
class="artist text-ellipsis overflow-hidden whitespace-nowrap block text-[0.9rem] !text-k-text-secondary hover:!text-k-accent"
|
||||
:href="`/#/artist/${song.artist_id}`"
|
||||
class="artist text-ellipsis overflow-hidden whitespace-nowrap block text-[0.9rem] !text-k-text-secondary hover:!text-k-accent"
|
||||
>
|
||||
{{ song.artist_name }}
|
||||
</a>
|
||||
|
@ -29,7 +29,7 @@ const { startDragging } = useDraggable('songs')
|
|||
const song = requireInjection(CurrentSongKey, ref())
|
||||
|
||||
const cover = computed(() => song.value?.album_cover || defaultCover)
|
||||
const coverBackgroundImage = computed(() => `url(${ cover.value })`)
|
||||
const coverBackgroundImage = computed(() => `url(${cover.value})`)
|
||||
const draggable = computed(() => Boolean(song.value))
|
||||
|
||||
const onDragStart = (event: DragEvent) => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`renders with current song 1`] = `
|
||||
<div data-v-91ed60f7="" class="playing song-info px-6 py-0 flex items-center content-start w-[84px] md:w-80 gap-5" draggable="true"><span data-v-91ed60f7="" class="album-thumb block h-[55%] md:h-3/4 aspect-square rounded-full bg-cover"></span>
|
||||
<div data-v-91ed60f7="" class="meta overflow-hidden hidden md:block">
|
||||
<h3 data-v-91ed60f7="" class="title text-ellipsis overflow-hidden whitespace-nowrap">Fahrstuhl zum Mond</h3><a data-v-91ed60f7="" class="artist text-ellipsis overflow-hidden whitespace-nowrap block text-[0.9rem] !text-k-text-secondary hover:!text-k-accent" href="/#/artist/10">Led Zeppelin</a>
|
||||
<h3 data-v-91ed60f7="" class="title text-ellipsis overflow-hidden whitespace-nowrap">Fahrstuhl zum Mond</h3><a data-v-91ed60f7="" href="/#/artist/10" class="artist text-ellipsis overflow-hidden whitespace-nowrap block text-[0.9rem] !text-k-text-secondary hover:!text-k-accent">Led Zeppelin</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<footer
|
||||
ref="root"
|
||||
class="flex flex-col relative z-20 bg-k-bg-secondary h-k-footer-height"
|
||||
@contextmenu.prevent="requestContextMenu"
|
||||
@mousemove="showControls"
|
||||
@contextmenu.prevent="requestContextMenu"
|
||||
>
|
||||
<AudioPlayer v-show="song" />
|
||||
|
||||
|
|
|
@ -9,6 +9,24 @@ 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('http://test/foo.jpg')
|
||||
|
||||
this.renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByTestId('album-art-overlay'))
|
||||
})
|
||||
|
||||
it('does not have a translucent over if configured not so', async () => {
|
||||
preferenceStore.state.show_album_art_overlay = false
|
||||
|
||||
this.renderComponent()
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('album-art-overlay')).toBeNull())
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(MainContent, {
|
||||
global: {
|
||||
|
@ -32,22 +50,4 @@ new class extends UnitTestCase {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('has a translucent overlay per album', async () => {
|
||||
this.mock(albumStore, 'fetchThumbnail').mockResolvedValue('http://test/foo.jpg')
|
||||
|
||||
this.renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByTestId('album-art-overlay'))
|
||||
})
|
||||
|
||||
it('does not have a translucent over if configured not so', async () => {
|
||||
preferenceStore.state.show_album_art_overlay = false
|
||||
|
||||
this.renderComponent()
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('album-art-overlay')).toBeNull())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</ExtraDrawerButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus } from '@/utils'
|
||||
import { useNewVersionNotification } from '@/composables'
|
||||
|
|
|
@ -9,32 +9,6 @@ import { eventBus } from '@/utils'
|
|||
import ExtraDrawer from './ExtraDrawer.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (songRef: Ref<Song | null> = ref(null)): [RenderResult, Mock, Mock] {
|
||||
const artist = factory<Artist>('artist')
|
||||
const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist)
|
||||
|
||||
const album = factory<Album>('album')
|
||||
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
|
||||
|
||||
const rendered = this.render(ExtraDrawer, {
|
||||
global: {
|
||||
stubs: {
|
||||
ProfileAvatar: this.stub(),
|
||||
LyricsPane: this.stub('lyrics'),
|
||||
AlbumInfo: this.stub('album-info'),
|
||||
ArtistInfo: this.stub('artist-info'),
|
||||
YouTubeVideoList: this.stub('youtube-video-list'),
|
||||
ExtraPanelTabHeader: this.stub()
|
||||
},
|
||||
provide: {
|
||||
[<symbol>CurrentSongKey]: songRef
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return [rendered, resolveArtistMock, resolveAlbumMock]
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders without a current song', () => expect(this.renderComponent()[0].html()).toMatchSnapshot())
|
||||
|
||||
|
@ -90,4 +64,30 @@ new class extends UnitTestCase {
|
|||
expect(emitMock).toHaveBeenCalledWith('LOG_OUT')
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (songRef: Ref<Song | null> = ref(null)): [RenderResult, Mock, Mock] {
|
||||
const artist = factory<Artist>('artist')
|
||||
const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist)
|
||||
|
||||
const album = factory<Album>('album')
|
||||
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
|
||||
|
||||
const rendered = this.render(ExtraDrawer, {
|
||||
global: {
|
||||
stubs: {
|
||||
ProfileAvatar: this.stub(),
|
||||
LyricsPane: this.stub('lyrics'),
|
||||
AlbumInfo: this.stub('album-info'),
|
||||
ArtistInfo: this.stub('artist-info'),
|
||||
YouTubeVideoList: this.stub('youtube-video-list'),
|
||||
ExtraPanelTabHeader: this.stub()
|
||||
},
|
||||
provide: {
|
||||
[<symbol>CurrentSongKey]: songRef
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return [rendered, resolveArtistMock, resolveAlbumMock]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
<div
|
||||
v-show="activeTab === 'YouTube'"
|
||||
id="extraPanelYouTube"
|
||||
data-testid="extra-drawer-youtube"
|
||||
aria-labelledby="extraTabYouTube"
|
||||
data-testid="extra-drawer-youtube"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</ExtraDrawerButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faArrowRightFromBracket } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
|
|
|
@ -22,12 +22,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
faClockRotateLeft,
|
||||
faHeart,
|
||||
faUsers,
|
||||
faWandMagicSparkles
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faClockRotateLeft, faHeart, faUsers, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ListMusic } from 'lucide-vue-next'
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<SidebarItem
|
||||
screen="Queue"
|
||||
href="#/queue"
|
||||
:class="droppable && 'droppable'"
|
||||
href="#/queue"
|
||||
screen="Queue"
|
||||
@dragleave="onQueueDragLeave"
|
||||
@dragover.prevent="onQueueDragOver"
|
||||
@drop="onQueueDrop"
|
||||
@dragover.prevent="onQueueDragOver"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon :icon="faListOl" fixed-width />
|
||||
|
@ -39,7 +39,7 @@ const onQueueDrop = async (event: DragEvent) => {
|
|||
|
||||
if (songs.length) {
|
||||
queueStore.queue(songs)
|
||||
toastSuccess(`Added ${ pluralize(songs, 'song') } to queue.`)
|
||||
toastSuccess(`Added ${pluralize(songs, 'song')} to queue.`)
|
||||
} else {
|
||||
toastWarning('No applicable songs to queue.')
|
||||
}
|
||||
|
|
|
@ -27,13 +27,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import {
|
||||
useAuthorization,
|
||||
useKoelPlus,
|
||||
useRouter,
|
||||
useUpload,
|
||||
useLocalStorage
|
||||
} from '@/composables'
|
||||
import { useAuthorization, useKoelPlus, useLocalStorage, useRouter, useUpload } from '@/composables'
|
||||
|
||||
import SidebarPlaylistsSection from './SidebarPlaylistsSection.vue'
|
||||
import SearchForm from '@/components/ui/SearchForm.vue'
|
||||
|
|
|
@ -5,19 +5,6 @@ import { faHome } from '@fortawesome/free-solid-svg-icons'
|
|||
import SidebarItem from './SidebarItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(SidebarItem, {
|
||||
props: {
|
||||
icon: faHome,
|
||||
href: '#',
|
||||
screen: 'Home'
|
||||
},
|
||||
slots: {
|
||||
default: 'Home'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
|
@ -32,4 +19,17 @@ new class extends UnitTestCase {
|
|||
expect(screen.getByTestId('sidebar-item').classList.contains('current')).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(SidebarItem, {
|
||||
props: {
|
||||
icon: faHome,
|
||||
href: '#',
|
||||
screen: 'Home'
|
||||
},
|
||||
slots: {
|
||||
default: 'Home'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
</template>
|
||||
|
||||
<ul class="menu">
|
||||
<SidebarItem v-if="isAdmin" screen="Settings" href="#/settings">
|
||||
<SidebarItem v-if="isAdmin" href="#/settings" screen="Settings">
|
||||
<template #icon>
|
||||
<Icon :icon="faTools" fixed-width />
|
||||
</template>
|
||||
Settings
|
||||
</SidebarItem>
|
||||
<SidebarItem v-if="allowsUpload" screen="Upload" href="#/upload">
|
||||
<SidebarItem v-if="allowsUpload" href="#/upload" screen="Upload">
|
||||
<template #icon>
|
||||
<Icon :icon="faUpload" fixed-width />
|
||||
</template>
|
||||
Upload
|
||||
</SidebarItem>
|
||||
<SidebarItem v-if="isAdmin" screen="Users" href="#/users">
|
||||
<SidebarItem v-if="isAdmin" href="#/users" screen="Users">
|
||||
<template #icon>
|
||||
<Icon :icon="faUsers" fixed-width />
|
||||
</template>
|
||||
|
@ -26,7 +26,7 @@
|
|||
</ul>
|
||||
</SidebarSection>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faTools, faUpload, faUsers } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useAuthorization, useUpload } from '@/composables'
|
||||
|
||||
|
|
|
@ -8,17 +8,6 @@ import PlaylistSidebarItem from './PlaylistSidebarItem.vue'
|
|||
import PlaylistFolderSidebarItem from './PlaylistFolderSidebarItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
this.render(SidebarPlaylistsSection, {
|
||||
global: {
|
||||
stubs: {
|
||||
PlaylistSidebarItem,
|
||||
PlaylistFolderSidebarItem
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('displays orphan playlists', () => {
|
||||
playlistStore.state.playlists = [
|
||||
|
@ -44,4 +33,15 @@ new class extends UnitTestCase {
|
|||
;['Foo Folder', 'Bar Folder'].forEach(text => screen.getByText(text))
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
this.render(SidebarPlaylistsSection, {
|
||||
global: {
|
||||
stubs: {
|
||||
PlaylistSidebarItem,
|
||||
PlaylistFolderSidebarItem
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ const playlists = toRef(playlistStore.state, 'playlists')
|
|||
const favorites = toRef(favoriteStore.state, 'songs')
|
||||
|
||||
const orphanPlaylists = computed(() => playlists.value.filter(({ folder_id }) => {
|
||||
if (folder_id === null) return true
|
||||
if (folder_id === null) return true
|
||||
|
||||
// if the playlist's folder is not found, it's an orphan
|
||||
// this can happen if the playlist belongs to another user (collaborative playlist)
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
justify-center z-10 text-k-text-secondary bg-k-bg-secondary border-[1.5px] border-white/20 cursor-pointer
|
||||
hover:text-k-text-primary hover:bg-k-bg-secondary"
|
||||
>
|
||||
<input v-model="value" type="checkbox" class="hidden">
|
||||
<input v-model="value" class="hidden" type="checkbox">
|
||||
<Icon v-if="value" :icon="faAngleLeft" />
|
||||
<Icon v-else :icon="faAngleRight" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
|
|
|
@ -5,32 +5,32 @@
|
|||
</template>
|
||||
|
||||
<ul class="menu">
|
||||
<SidebarItem screen="Home" href="#/home">
|
||||
<SidebarItem href="#/home" screen="Home">
|
||||
<template #icon>
|
||||
<Icon :icon="faHome" fixed-width />
|
||||
</template>
|
||||
Home
|
||||
</SidebarItem>
|
||||
<QueueSidebarItem />
|
||||
<SidebarItem screen="Songs" href="#/songs">
|
||||
<SidebarItem href="#/songs" screen="Songs">
|
||||
<template #icon>
|
||||
<Icon :icon="faMusic" fixed-width />
|
||||
</template>
|
||||
All Songs
|
||||
</SidebarItem>
|
||||
<SidebarItem screen="Albums" href="#/albums">
|
||||
<SidebarItem href="#/albums" screen="Albums">
|
||||
<template #icon>
|
||||
<Icon :icon="faCompactDisc" fixed-width />
|
||||
</template>
|
||||
Albums
|
||||
</SidebarItem>
|
||||
<SidebarItem screen="Artists" href="#/artists">
|
||||
<SidebarItem href="#/artists" screen="Artists">
|
||||
<template #icon>
|
||||
<Icon :icon="faMicrophone" fixed-width />
|
||||
</template>
|
||||
Artists
|
||||
</SidebarItem>
|
||||
<SidebarItem screen="Genres" href="#/genres">
|
||||
<SidebarItem href="#/genres" screen="Genres">
|
||||
<template #icon>
|
||||
<Icon :icon="faTags" fixed-width />
|
||||
</template>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</SidebarSection>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faCompactDisc, faHome, faMicrophone, faMusic, faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { unescape } from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<SidebarItem screen="YouTube" href="#/youtube">
|
||||
<SidebarItem href="#/youtube" screen="YouTube">
|
||||
<template #icon>
|
||||
<Icon :icon="faYoutube" fixed-width />
|
||||
</template>
|
||||
|
|
|
@ -6,16 +6,6 @@ import { screen, waitFor } from '@testing-library/vue'
|
|||
import AboutKoelModel from './AboutKoelModal.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(AboutKoelModel, {
|
||||
global: {
|
||||
stubs: {
|
||||
SponsorList: this.stub('sponsor-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
commonStore.state.current_version = 'v0.0.0'
|
||||
|
@ -44,4 +34,14 @@ new class extends UnitTestCase {
|
|||
window.IS_DEMO = false
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(AboutKoelModel, {
|
||||
global: {
|
||||
stubs: {
|
||||
SponsorList: this.stub('sponsor-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
<main class="p-6">
|
||||
<div class="mb-4">
|
||||
<img alt="Koel's logo" src="@/../img/logo.svg" width="128" class="inline-block">
|
||||
<img alt="Koel's logo" class="inline-block" src="@/../img/logo.svg" width="128">
|
||||
</div>
|
||||
|
||||
<div class="current-version">
|
||||
|
@ -57,7 +57,7 @@
|
|||
</main>
|
||||
|
||||
<footer>
|
||||
<Btn data-testid="close-modal-btn" danger rounded @click.prevent="close">Close</Btn>
|
||||
<Btn danger data-testid="close-modal-btn" rounded @click.prevent="close">Close</Btn>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { orderBy } from 'lodash'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { http } from '@/services'
|
||||
|
@ -26,7 +26,7 @@ onMounted(async () => {
|
|||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
li&:last-child {
|
||||
&::before {
|
||||
content: ', and '
|
||||
|
|
|
@ -22,16 +22,6 @@ new class extends UnitTestCase {
|
|||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
preferenceStore.initialized.value = true
|
||||
const rendered = this.render(SupportKoel)
|
||||
|
||||
vi.advanceTimersByTime(30 * 60 * 1000)
|
||||
await this.tick()
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('shows after a delay', async () => expect((await this.renderComponent()).html()).toMatchSnapshot())
|
||||
|
||||
|
@ -61,4 +51,14 @@ new class extends UnitTestCase {
|
|||
expect(preferenceStore.state.support_bar_no_bugging).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
preferenceStore.initialized.value = true
|
||||
const rendered = this.render(SupportKoel)
|
||||
|
||||
vi.advanceTimersByTime(30 * 60 * 1000)
|
||||
await this.tick()
|
||||
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`renders 1`] = `
|
||||
<div data-v-6b5b01a9="" class="about text-k-text-secondary text-center max-w-[480px] overflow-hidden relative" data-testid="about-koel" tabindex="0">
|
||||
<main data-v-6b5b01a9="" class="p-6">
|
||||
<div data-v-6b5b01a9="" class="mb-4"><img data-v-6b5b01a9="" alt="Koel's logo" src="undefined/resources/assets/img/logo.svg" width="128" class="inline-block"></div>
|
||||
<div data-v-6b5b01a9="" class="mb-4"><img data-v-6b5b01a9="" alt="Koel's logo" class="inline-block" src="undefined/resources/assets/img/logo.svg" width="128"></div>
|
||||
<div data-v-6b5b01a9="" class="current-version"> Koel v0.0.0 <span data-v-6b5b01a9="">Community</span> Edition
|
||||
<!--v-if-->
|
||||
</div>
|
||||
|
@ -12,6 +12,6 @@ exports[`renders 1`] = `
|
|||
<!--v-if--><br data-v-6b5b01a9="" data-testid="sponsor-list">
|
||||
<p data-v-6b5b01a9=""> Loving Koel? Please consider supporting its development via <a data-v-6b5b01a9="" href="https://github.com/users/phanan/sponsorship" rel="noopener" target="_blank">GitHub Sponsors</a> and/or <a data-v-6b5b01a9="" href="https://opencollective.com/koel" rel="noopener" target="_blank">OpenCollective</a>. </p>
|
||||
</main>
|
||||
<footer data-v-6b5b01a9=""><button data-v-8943c846="" data-v-6b5b01a9="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer" type="button" data-testid="close-modal-btn" danger="" rounded="">Close</button></footer>
|
||||
<footer data-v-6b5b01a9=""><button data-v-8943c846="" data-v-6b5b01a9="" class="text-base text-k-text-primary bg-k-primary px-4 py-2.5 rounded cursor-pointer" type="button" danger="" data-testid="close-modal-btn" rounded="">Close</button></footer>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -7,12 +7,6 @@ import { Events } from '@/config'
|
|||
import CreateNewPlaylistContextMenu from './CreatePlaylistContextMenu.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
this.render(CreateNewPlaylistContextMenu)
|
||||
eventBus.emit('CREATE_NEW_PLAYLIST_CONTEXT_MENU_REQUESTED', { top: 420, left: 42 })
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it.each<[string, keyof Events]>([
|
||||
['playlist-context-menu-create-simple', 'MODAL_SHOW_CREATE_PLAYLIST_FORM'],
|
||||
|
@ -25,4 +19,10 @@ new class extends UnitTestCase {
|
|||
await waitFor(() => expect(emitMock).toHaveBeenCalledWith(eventName))
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
this.render(CreateNewPlaylistContextMenu)
|
||||
eventBus.emit('CREATE_NEW_PLAYLIST_CONTEXT_MENU_REQUESTED', { top: 420, left: 42 })
|
||||
await this.tick(2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<header>
|
||||
<h1>
|
||||
New Playlist
|
||||
<span v-if="songs.length" data-testid="from-songs" class="text-k-text-secondary">
|
||||
<span v-if="songs.length" class="text-k-text-secondary" data-testid="from-songs">
|
||||
from {{ pluralize(songs, 'song') }}
|
||||
</span>
|
||||
</h1>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span>
|
||||
<Btn v-if="shouldShowInviteButton" success small @click.prevent="inviteCollaborators">Invite</Btn>
|
||||
<Btn v-if="shouldShowInviteButton" small success @click.prevent="inviteCollaborators">Invite</Btn>
|
||||
<span v-if="justCreatedInviteLink" class="text-k-text-secondary text-[0.95rem]">
|
||||
<Icon :icon="faCheckCircle" class="text-k-success mr-1" />
|
||||
Link copied to clipboard!
|
||||
|
@ -9,7 +9,7 @@
|
|||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faCheckCircle, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import { copyText } from '@/utils'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { it, expect } from 'vitest'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { ref } from 'vue'
|
||||
|
@ -7,8 +7,8 @@ import Modal from './PlaylistCollaborationModal.vue'
|
|||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it ('renders the modal', async () => {
|
||||
const { html } = this.render(Modal, {
|
||||
it('renders the modal', async () => {
|
||||
const { html } = this.render(Modal, {
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>ModalContextKey]: [ref({ playlist: factory<Playlist>('playlist') })]
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAuthorization, useModal } from '@/composables'
|
||||
|
||||
|
|
|
@ -5,6 +5,22 @@ import { playlistCollaborationService } from '@/services'
|
|||
import Component from './PlaylistCollaboratorList.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
const playlist = factory<Playlist>('playlist', {
|
||||
is_collaborative: true
|
||||
})
|
||||
|
||||
const fetchMock = this.mock(playlistCollaborationService, 'fetchCollaborators').mockResolvedValue(
|
||||
factory<PlaylistCollaborator>('playlist-collaborator', 5)
|
||||
)
|
||||
|
||||
const { html } = await this.be().renderComponent(playlist)
|
||||
expect(fetchMock).toHaveBeenCalledWith(playlist)
|
||||
expect(html()).toMatchSnapshot()
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (playlist: Playlist) {
|
||||
const rendered = this.render(Component, {
|
||||
props: {
|
||||
|
@ -21,20 +37,4 @@ new class extends UnitTestCase {
|
|||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
const playlist = factory<Playlist>('playlist', {
|
||||
is_collaborative: true
|
||||
})
|
||||
|
||||
const fetchMock = this.mock(playlistCollaborationService, 'fetchCollaborators').mockResolvedValue(
|
||||
factory<PlaylistCollaborator>('playlist-collaborator', 5)
|
||||
)
|
||||
|
||||
const { html } = await this.be().renderComponent(playlist)
|
||||
expect(fetchMock).toHaveBeenCalledWith(playlist)
|
||||
expect(html()).toMatchSnapshot()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { sortBy } from 'lodash'
|
||||
import { computed, onMounted, ref, Ref, toRefs } from 'vue'
|
||||
import { useAuthorization, useDialogBox, useErrorHandler } from '@/composables'
|
||||
|
|
|
@ -5,22 +5,6 @@ import factory from '@/__tests__/factory'
|
|||
import Component from './PlaylistCollaboratorListItem.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (props: {
|
||||
collaborator: PlaylistCollaborator,
|
||||
removable: boolean,
|
||||
manageable: boolean,
|
||||
role: 'owner' | 'contributor'
|
||||
}) {
|
||||
return this.render(Component, {
|
||||
props,
|
||||
global: {
|
||||
stubs: {
|
||||
UserAvatar: this.stub('UserAvatar')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('does not show a badge when current user is not the collaborator', async () => {
|
||||
const currentUser = factory<User>('user')
|
||||
|
@ -88,4 +72,20 @@ new class extends UnitTestCase {
|
|||
expect(emitted('remove')).toBeTruthy()
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (props: {
|
||||
collaborator: PlaylistCollaborator,
|
||||
removable: boolean,
|
||||
manageable: boolean,
|
||||
role: 'owner' | 'contributor'
|
||||
}) {
|
||||
return this.render(Component, {
|
||||
props,
|
||||
global: {
|
||||
stubs: {
|
||||
UserAvatar: this.stub('UserAvatar')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
<span v-else class="contributor">Contributor</span>
|
||||
</span>
|
||||
<span v-if="manageable" class="actions flex-[0_0_72px] text-right">
|
||||
<Btn v-if="removable" small danger @click.prevent="emit('remove')">Remove</Btn>
|
||||
<Btn v-if="removable" danger small @click.prevent="emit('remove')">Remove</Btn>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faCircleCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toRefs } from 'vue'
|
||||
|
||||
|
@ -46,7 +46,7 @@ const { currentUser } = useAuthorization()
|
|||
const emit = defineEmits<{ (e: 'remove'): void }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
span {
|
||||
@apply inline-block min-w-0 leading-normal;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="inline-block align-middle">
|
||||
<ul class="align-middle -space-x-2">
|
||||
<li v-for="user in displayedCollaborators" :key="user.id" class="inline-block align-baseline">
|
||||
<UserAvatar :user="user" width="24" class="border border-white/30" />
|
||||
<UserAvatar :user="user" class="border border-white/30" width="24" />
|
||||
</li>
|
||||
</ul>
|
||||
<span v-if="remainderCount" class="ml-2">
|
||||
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/user/UserAvatar.vue'
|
||||
|
|
|
@ -4,23 +4,12 @@ import UnitTestCase from '@/__tests__/UnitTestCase'
|
|||
import { eventBus } from '@/utils'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import { songStore, userStore } from '@/stores'
|
||||
import { queueStore, songStore, userStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { queueStore } from '@/stores'
|
||||
import { MessageToasterStub } from '@/__tests__/stubs'
|
||||
import PlaylistContextMenu from './PlaylistContextMenu.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (playlist: Playlist, user: User | null = null) {
|
||||
userStore.state.current = user || factory<User>('user', {
|
||||
id: playlist.user_id
|
||||
})
|
||||
|
||||
this.render(PlaylistContextMenu)
|
||||
eventBus.emit('PLAYLIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, playlist)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('edits a standard playlist', async () => {
|
||||
const playlist = factory<Playlist>('playlist')
|
||||
|
@ -164,4 +153,14 @@ new class extends UnitTestCase {
|
|||
expect(emitMock).toHaveBeenCalledWith('MODAL_SHOW_PLAYLIST_COLLABORATION', playlist)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (playlist: Playlist, user: User | null = null) {
|
||||
userStore.state.current = user || factory<User>('user', {
|
||||
id: playlist.user_id
|
||||
})
|
||||
|
||||
this.render(PlaylistContextMenu)
|
||||
eventBus.emit('PLAYLIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, playlist)
|
||||
await this.tick(2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { usePolicies, useContextMenu, useMessageToaster, useKoelPlus, useRouter } from '@/composables'
|
||||
import { useContextMenu, useKoelPlus, useMessageToaster, usePolicies, useRouter } from '@/composables'
|
||||
import { playbackService } from '@/services'
|
||||
import { songStore, queueStore } from '@/stores'
|
||||
import { queueStore, songStore } from '@/stores'
|
||||
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const { go } = useRouter()
|
||||
|
|
|
@ -10,12 +10,6 @@ import { MessageToasterStub } from '@/__tests__/stubs'
|
|||
import PlaylistFolderContextMenu from './PlaylistFolderContextMenu.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (folder: PlaylistFolder) {
|
||||
this.render(PlaylistFolderContextMenu)
|
||||
eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, folder)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renames', async () => {
|
||||
const folder = factory<PlaylistFolder>('playlist-folder')
|
||||
|
@ -120,6 +114,12 @@ new class extends UnitTestCase {
|
|||
})
|
||||
}
|
||||
|
||||
private async renderComponent (folder: PlaylistFolder) {
|
||||
this.render(PlaylistFolderContextMenu)
|
||||
eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, folder)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
private createPlayableFolder () {
|
||||
const folder = factory<PlaylistFolder>('playlist-folder')
|
||||
this.mock(playlistStore, 'byFolder', factory<Playlist>('playlist', 3, { folder_id: folder.id }))
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
:is-first-group="index === 0"
|
||||
@input="onGroupChanged"
|
||||
/>
|
||||
<Btn class="btn-add-group" success small title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<Btn class="btn-add-group" small success title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<Icon :icon="faPlus" />
|
||||
Group
|
||||
</Btn>
|
||||
|
@ -55,7 +55,8 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
|||
import { ref, toRef } from 'vue'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import {
|
||||
useDialogBox, useErrorHandler,
|
||||
useDialogBox,
|
||||
useErrorHandler,
|
||||
useKoelPlus,
|
||||
useMessageToaster,
|
||||
useModal,
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
:is-first-group="index === 0"
|
||||
@input="onGroupChanged"
|
||||
/>
|
||||
<Btn class="btn-add-group" success small title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<Btn class="btn-add-group" small success title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<Icon :icon="faPlus" />
|
||||
Group
|
||||
</Btn>
|
||||
|
@ -41,7 +41,8 @@
|
|||
|
||||
<div v-if="isPlus" class="form-row">
|
||||
<label class="text-k-text-secondary">
|
||||
<CheckBox v-model="mutablePlaylist.own_songs_only" /> Only include songs from my own library
|
||||
<CheckBox v-model="mutablePlaylist.own_songs_only" />
|
||||
Only include songs from my own library
|
||||
</label>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -77,7 +78,7 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
|
|||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const {isPlus} = useKoelPlus()
|
||||
const { isPlus } = useKoelPlus()
|
||||
|
||||
const playlist = useModal().getFromContext<Playlist>('playlist')
|
||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||
|
|
|
@ -23,11 +23,11 @@
|
|||
|
||||
<div class="text-center absolute w-full left-0 -mt-[2px]">
|
||||
<Btn
|
||||
title="Remove this rule"
|
||||
class="aspect-square scale-75 hover:scale-90 active:scale-[80%]"
|
||||
rounded
|
||||
success
|
||||
small
|
||||
success
|
||||
title="Remove this rule"
|
||||
@click.prevent="addRule"
|
||||
>
|
||||
<Icon :icon="faPlus" />
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { faRefresh, faTimes, faUpload } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import { useFileDialog } from '@vueuse/core'
|
||||
|
@ -76,7 +76,7 @@ const onCrop = (result: string) => {
|
|||
const onCancel = () => (cropperSource.value = null)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style lang="postcss" scoped>
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import LastfmIntegration from '@/components/profile-preferences/LastfmIntegration.vue'
|
||||
import SpotifyIntegration from '@/components/profile-preferences/SpotifyIntegration.vue'
|
||||
</script>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
Last.fm integration is not enabled.
|
||||
<span v-if="isAdmin" data-testid="lastfm-admin-instruction">
|
||||
Check
|
||||
<a href="https://docs.koel.dev/service-integrations#last-fm" class="text-k-highlight" target="_blank">
|
||||
<a class="text-k-highlight" href="https://docs.koel.dev/service-integrations#last-fm" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
for instructions.
|
||||
|
|
|
@ -7,10 +7,6 @@ import { MessageToasterStub } from '@/__tests__/stubs'
|
|||
import ProfileForm from './ProfileForm.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (user: User) {
|
||||
return this.be(user).render(ProfileForm)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('updates profile', async () => {
|
||||
const updateMock = this.mock(authService, 'updateProfile')
|
||||
|
@ -37,4 +33,8 @@ new class extends UnitTestCase {
|
|||
expect(alertMock).toHaveBeenCalledWith('Profile updated.')
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (user: User) {
|
||||
return this.be(user).render(ProfileForm)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
<TextInput
|
||||
v-model="profile.current_password"
|
||||
v-koel-focus
|
||||
data-testid="currentPassword"
|
||||
name="current_password"
|
||||
placeholder="Required to update your profile"
|
||||
required
|
||||
type="password"
|
||||
data-testid="currentPassword"
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
|
@ -73,7 +73,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { authService, UpdateCurrentProfileData } from '@/services'
|
||||
import { useMessageToaster, useAuthorization, useErrorHandler } from '@/composables'
|
||||
import { useAuthorization, useErrorHandler, useMessageToaster } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<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>
|
||||
<a class="text-k-highlight" href="https://koel.dev/#mobile" target="_blank">Koel Player</a>
|
||||
on your mobile device.<br>
|
||||
The QR code will refresh every 10 minutes.
|
||||
<img class="mt-4 rounded-4" :src="qrCodeUrl" alt="QR Code" width="192" height="192">
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="mt-4 rounded-4" height="192" width="192">
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useQRCode } from '@vueuse/integrations/useQRCode'
|
||||
import { authService } from '@/services'
|
||||
|
|
|
@ -9,14 +9,6 @@ const theme: Theme = {
|
|||
}
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(ThemeCard, {
|
||||
props: {
|
||||
theme
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
|
||||
|
||||
|
@ -28,4 +20,12 @@ new class extends UnitTestCase {
|
|||
expect(emitted().selected[0]).toEqual([theme])
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
return this.render(ThemeCard, {
|
||||
props: {
|
||||
theme
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
class="theme h-[96px] bg-center bg-cover relative cursor-pointer rounded-lg overflow-hidden border-2 border-solid border-white/10 transition duration-300 hover:border-white/50]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="opacity-0 hover:opacity-100 absolute h-full w-full top-0 left-0 flex items-center justify-center text-lg transition-opacity bg-black/20"
|
||||
type="button"
|
||||
@click="$emit('selected', theme)"
|
||||
>
|
||||
{{ name }}
|
||||
|
|
|
@ -1,3 +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>`;
|
||||
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 class="text-k-highlight" href="https://koel.dev/#mobile" target="_blank">Koel Player</a> on your mobile device.<br> The QR code will refresh every 10 minutes. <img src="data:image/png;base64,my-qr-code" alt="QR Code" class="mt-4 rounded-4" height="192" width="192"></article>`;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<article data-v-1467c50f="" class="theme h-[96px] bg-center bg-cover relative cursor-pointer rounded-lg overflow-hidden border-2 border-solid border-white/10 transition duration-300 hover:border-white/50]" style="background-color: rgb(255, 0, 0);" title="Set current theme to Sample"><button data-v-1467c50f="" type="button" class="opacity-0 hover:opacity-100 absolute h-full w-full top-0 left-0 flex items-center justify-center text-lg transition-opacity bg-black/20">Sample</button></article>`;
|
||||
exports[`renders 1`] = `<article data-v-1467c50f="" class="theme h-[96px] bg-center bg-cover relative cursor-pointer rounded-lg overflow-hidden border-2 border-solid border-white/10 transition duration-300 hover:border-white/50]" style="background-color: rgb(255, 0, 0);" title="Set current theme to Sample"><button data-v-1467c50f="" class="opacity-0 hover:opacity-100 absolute h-full w-full top-0 left-0 flex items-center justify-center text-lg transition-opacity bg-black/20" type="button">Sample</button></article>`;
|
||||
|
|
|
@ -10,20 +10,6 @@ new class extends UnitTestCase {
|
|||
super.beforeEach(() => this.mock(albumStore, 'paginate'))
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
albumStore.state.albums = factory<Album>('album', 9)
|
||||
|
||||
this.render(AlbumListScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumCard: this.stub('album-card')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.router.activateRoute({ path: 'albums', screen: 'Albums' })
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
await this.renderComponent()
|
||||
|
@ -55,4 +41,18 @@ new class extends UnitTestCase {
|
|||
await waitFor(() => expect(screen.getByTestId('album-grid').classList.contains(`as-thumbnails`)).toBe(true))
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
albumStore.state.albums = factory<Album>('album', 9)
|
||||
|
||||
this.render(AlbumListScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
AlbumCard: this.stub('album-card')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.router.activateRoute({ path: 'albums', screen: 'Albums' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,44 +11,6 @@ import AlbumScreen from './AlbumScreen.vue'
|
|||
let album: Album
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
commonStore.state.uses_last_fm = true
|
||||
|
||||
album = factory<Album>('album', {
|
||||
id: 42,
|
||||
name: 'Led Zeppelin IV',
|
||||
artist_id: 123,
|
||||
artist_name: 'Led Zeppelin'
|
||||
})
|
||||
|
||||
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
|
||||
|
||||
const songs = factory<Song>('song', 13)
|
||||
const fetchSongsMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: 'albums/42',
|
||||
screen: 'Album'
|
||||
}, { id: '42' })
|
||||
|
||||
this.render(AlbumScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list'),
|
||||
AlbumCard: this.stub('album-card'),
|
||||
AlbumInfo: this.stub('album-info')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resolveAlbumMock).toHaveBeenCalledWith(album.id)
|
||||
expect(fetchSongsMock).toHaveBeenCalledWith(album.id)
|
||||
})
|
||||
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('downloads', async () => {
|
||||
const downloadMock = this.mock(downloadService, 'fromAlbum')
|
||||
|
@ -91,4 +53,42 @@ new class extends UnitTestCase {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
commonStore.state.uses_last_fm = true
|
||||
|
||||
album = factory<Album>('album', {
|
||||
id: 42,
|
||||
name: 'Led Zeppelin IV',
|
||||
artist_id: 123,
|
||||
artist_name: 'Led Zeppelin'
|
||||
})
|
||||
|
||||
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
|
||||
|
||||
const songs = factory<Song>('song', 13)
|
||||
const fetchSongsMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: 'albums/42',
|
||||
screen: 'Album'
|
||||
}, { id: '42' })
|
||||
|
||||
this.render(AlbumScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list'),
|
||||
AlbumCard: this.stub('album-card'),
|
||||
AlbumInfo: this.stub('album-info')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resolveAlbumMock).toHaveBeenCalledWith(album.id)
|
||||
expect(fetchSongsMock).toHaveBeenCalledWith(album.id)
|
||||
})
|
||||
|
||||
await this.tick(2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,15 +44,15 @@
|
|||
<template #header>
|
||||
<label :class="{ active: activeTab === 'Songs' }">
|
||||
Songs
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Songs">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="Songs">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'OtherAlbums' }">
|
||||
Other Albums
|
||||
<input v-model="activeTab" type="radio" name="tab" value="OtherAlbums">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="OtherAlbums">
|
||||
</label>
|
||||
<label v-if="useLastfm" :class="{ active: activeTab === 'Info' }">
|
||||
Information
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Info">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="Info">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
@ -70,7 +70,7 @@
|
|||
<div v-show="activeTab === 'OtherAlbums'" class="albums-pane" data-testid="albums-pane">
|
||||
<template v-if="otherAlbums">
|
||||
<AlbumGrid v-if="otherAlbums.length" v-koel-overflow-fade view-mode="list">
|
||||
<AlbumCard v-for="otherAlbum in otherAlbums" :key="otherAlbum.id" layout="compact" :album="otherAlbum" />
|
||||
<AlbumCard v-for="otherAlbum in otherAlbums" :key="otherAlbum.id" :album="otherAlbum" layout="compact" />
|
||||
</AlbumGrid>
|
||||
<p v-else class="text-k-text-secondary p-6">
|
||||
No other albums by {{ album.artist_name }} found in the library.
|
||||
|
@ -81,7 +81,7 @@
|
|||
</AlbumGrid>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && album" class="info-pane">
|
||||
<div v-if="useLastfm && album" v-show="activeTab === 'Info'" class="info-pane">
|
||||
<AlbumInfo :album="album" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
|
|
|
@ -16,32 +16,6 @@ new class extends UnitTestCase {
|
|||
this.be()
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
const fetchMock = this.mock(songStore, 'paginate').mockResolvedValue(2)
|
||||
|
||||
this.router.$currentRoute.value = {
|
||||
screen: 'Songs',
|
||||
path: '/songs'
|
||||
}
|
||||
|
||||
const rendered = this.render(AllSongsScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith({
|
||||
sort: 'title',
|
||||
order: 'asc',
|
||||
page: 1,
|
||||
own_songs_only: false
|
||||
}))
|
||||
|
||||
return [rendered, fetchMock] as const
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
const [{ html }] = await this.renderComponent()
|
||||
|
@ -78,4 +52,30 @@ new class extends UnitTestCase {
|
|||
}))
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
const fetchMock = this.mock(songStore, 'paginate').mockResolvedValue(2)
|
||||
|
||||
this.router.$currentRoute.value = {
|
||||
screen: 'Songs',
|
||||
path: '/songs'
|
||||
}
|
||||
|
||||
const rendered = this.render(AllSongsScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith({
|
||||
sort: 'title',
|
||||
order: 'asc',
|
||||
page: 1,
|
||||
own_songs_only: false
|
||||
}))
|
||||
|
||||
return [rendered, fetchMock] as const
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,11 +67,12 @@ import { pluralize, secondsToHumanReadable } from '@/utils'
|
|||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import {
|
||||
useErrorHandler,
|
||||
useKoelPlus,
|
||||
useLocalStorage,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useSongListControls,
|
||||
useLocalStorage, useErrorHandler
|
||||
useSongListControls
|
||||
} from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
|
|
|
@ -10,21 +10,6 @@ new class extends UnitTestCase {
|
|||
super.beforeEach(() => this.mock(artistStore, 'paginate'))
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
artistStore.state.artists = factory<Artist>('artist', 9)
|
||||
|
||||
const rendered = this.render(ArtistListScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
ArtistCard: this.stub('artist-card')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.router.activateRoute({ path: 'artists', screen: 'Artists' })
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', async () => {
|
||||
await this.renderComponent()
|
||||
|
@ -56,4 +41,19 @@ new class extends UnitTestCase {
|
|||
await waitFor(() => expect(screen.getByTestId('artist-list').classList.contains(`as-thumbnails`)).toBe(true))
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
artistStore.state.artists = factory<Artist>('artist', 9)
|
||||
|
||||
const rendered = this.render(ArtistListScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
ArtistCard: this.stub('artist-card')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.router.activateRoute({ path: 'artists', screen: 'Artists' })
|
||||
return rendered
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,15 +43,15 @@
|
|||
<template #header>
|
||||
<label :class="{ active: activeTab === 'Songs' }">
|
||||
Songs
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Songs">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="Songs">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'Albums' }">
|
||||
Albums
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Albums">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="Albums">
|
||||
</label>
|
||||
<label v-if="useLastfm" :class="{ active: activeTab === 'Info' }">
|
||||
Information
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Info">
|
||||
<input v-model="activeTab" name="tab" type="radio" value="Info">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
@ -77,7 +77,7 @@
|
|||
</AlbumOrArtistGrid>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && artist" class="info-pane">
|
||||
<div v-if="useLastfm && artist" v-show="activeTab === 'Info'" class="info-pane">
|
||||
<ArtistInfo :artist="artist" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
|
@ -89,13 +89,7 @@ import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'
|
|||
import { eventBus, pluralize } from '@/utils'
|
||||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import {
|
||||
useErrorHandler,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useSongListControls,
|
||||
useThirdPartyServices
|
||||
} from '@/composables'
|
||||
import { useErrorHandler, useRouter, useSongList, useSongListControls, useThirdPartyServices } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ArtistThumbnail from '@/components/ui/ArtistAlbumThumbnail.vue'
|
||||
|
|
|
@ -6,15 +6,6 @@ import { favoriteStore } from '@/stores'
|
|||
import FavoritesScreen from './FavoritesScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
const fetchMock = this.mock(favoriteStore, 'fetch')
|
||||
this.render(FavoritesScreen)
|
||||
|
||||
await this.router.activateRoute({ path: 'favorites', screen: 'Favorites' })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders a list of favorites', async () => {
|
||||
favoriteStore.state.songs = factory<Song>('song', 13)
|
||||
|
@ -34,4 +25,13 @@ new class extends UnitTestCase {
|
|||
expect(screen.queryByTestId('song-list')).toBeNull()
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
const fetchMock = this.mock(favoriteStore, 'fetch')
|
||||
this.render(FavoritesScreen)
|
||||
|
||||
await this.router.activateRoute({ path: 'favorites', screen: 'Favorites' })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
class="rounded-[0.5em] inline-block m-1.5 align-middle overflow-hidden"
|
||||
>
|
||||
<a
|
||||
class="bg-white/15 inline-flex items-center justify-center !text-k-text-secondary
|
||||
transition-colors duration-200 ease-in-out hover:!text-k-text-primary hover:bg-k-highlight"
|
||||
:href="`/#/genres/${encodeURIComponent(genre.name)}`"
|
||||
:title="`${genre.name}: ${pluralize(genre.song_count, 'song')}`"
|
||||
class="bg-white/15 inline-flex items-center justify-center !text-k-text-secondary
|
||||
transition-colors duration-200 ease-in-out hover:!text-k-text-primary hover:bg-k-highlight"
|
||||
>
|
||||
<span class="name bg-white/5 px-[0.5em] py-[0.2em] leading-normal">{{ genre.name }}</span>
|
||||
<span class="count items-center px-[0.5em] py-[0.2em]">
|
||||
|
|
|
@ -7,6 +7,41 @@ import { playbackService } from '@/services'
|
|||
import GenreScreen from './GenreScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('renders the song list', async () => {
|
||||
await this.renderComponent()
|
||||
expect(screen.getByTestId('song-list')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shuffles all songs without fetching if genre has <= 500 songs', async () => {
|
||||
const genre = factory<Genre>('genre', { song_count: 10 })
|
||||
const songs = factory<Song>('song', 10)
|
||||
const playbackMock = this.mock(playbackService, 'queueAndPlay')
|
||||
|
||||
await this.renderComponent(genre, songs)
|
||||
|
||||
await this.user.click(screen.getByTitle('Shuffle all. Press Alt/⌥ to change mode.'))
|
||||
|
||||
expect(playbackMock).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('fetches and shuffles all songs if genre has > 500 songs', async () => {
|
||||
const genre = factory<Genre>('genre', { song_count: 501 })
|
||||
const songs = factory<Song>('song', 10) // we don't really need to generate 501 songs
|
||||
const playbackMock = this.mock(playbackService, 'queueAndPlay')
|
||||
const fetchMock = this.mock(songStore, 'fetchRandomForGenre').mockResolvedValue(songs)
|
||||
|
||||
await this.renderComponent(genre, songs)
|
||||
|
||||
await this.user.click(screen.getByTitle('Shuffle all. Press Alt/⌥ to change mode.'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledWith(genre, 500)
|
||||
expect(playbackMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (genre?: Genre, songs?: Song[]) {
|
||||
genre = genre || factory<Genre>('genre')
|
||||
|
||||
|
@ -42,39 +77,4 @@ new class extends UnitTestCase {
|
|||
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders the song list', async () => {
|
||||
await this.renderComponent()
|
||||
expect(screen.getByTestId('song-list')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shuffles all songs without fetching if genre has <= 500 songs', async () => {
|
||||
const genre = factory<Genre>('genre', { song_count: 10 })
|
||||
const songs = factory<Song>('song', 10)
|
||||
const playbackMock = this.mock(playbackService, 'queueAndPlay')
|
||||
|
||||
await this.renderComponent(genre, songs)
|
||||
|
||||
await this.user.click(screen.getByTitle('Shuffle all. Press Alt/⌥ to change mode.'))
|
||||
|
||||
expect(playbackMock).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('fetches and shuffles all songs if genre has > 500 songs', async () => {
|
||||
const genre = factory<Genre>('genre', { song_count: 501 })
|
||||
const songs = factory<Song>('song', 10) // we don't really need to generate 501 songs
|
||||
const playbackMock = this.mock(playbackService, 'queueAndPlay')
|
||||
const fetchMock = this.mock(songStore, 'fetchRandomForGenre').mockResolvedValue(songs)
|
||||
|
||||
await this.renderComponent(genre, songs)
|
||||
|
||||
await this.user.click(screen.getByTitle('Shuffle all. Press Alt/⌥ to change mode.'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledWith(genre, 500)
|
||||
expect(playbackMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,6 @@ import { screen } from '@testing-library/vue'
|
|||
import HomeScreen from './HomeScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
this.render(HomeScreen)
|
||||
await this.router.activateRoute({ path: 'home', screen: 'Home' })
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders an empty state if no songs found', async () => {
|
||||
commonStore.state.song_length = 0
|
||||
|
@ -54,4 +49,9 @@ new class extends UnitTestCase {
|
|||
expect(refreshMock).toHaveBeenCalled()
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent () {
|
||||
this.render(HomeScreen)
|
||||
await this.router.activateRoute({ path: 'home', screen: 'Home' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,17 @@
|
|||
|
||||
<div v-else class="space-y-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 w-full gap-8 md:gap-4">
|
||||
<MostPlayedSongs data-testid="most-played-songs" :loading="loading" />
|
||||
<RecentlyPlayedSongs data-testid="recently-played-songs" :loading="loading" />
|
||||
<MostPlayedSongs :loading="loading" data-testid="most-played-songs" />
|
||||
<RecentlyPlayedSongs :loading="loading" data-testid="recently-played-songs" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 w-full gap-8 md:gap-4">
|
||||
<RecentlyAddedAlbums data-testid="recently-added-albums" :loading="loading" />
|
||||
<RecentlyAddedSongs data-testid="recently-added-songs" :loading="loading" />
|
||||
<RecentlyAddedAlbums :loading="loading" data-testid="recently-added-albums" />
|
||||
<RecentlyAddedSongs :loading="loading" data-testid="recently-added-songs" />
|
||||
</div>
|
||||
|
||||
<MostPlayedArtists data-testid="most-played-artists" :loading="loading" />
|
||||
<MostPlayedAlbums data-testid="most-played-albums" :loading="loading" />
|
||||
<MostPlayedArtists :loading="loading" data-testid="most-played-artists" />
|
||||
<MostPlayedAlbums :loading="loading" data-testid="most-played-albums" />
|
||||
|
||||
<BtnScrollToTop />
|
||||
</div>
|
||||
|
|
|
@ -10,25 +10,6 @@ import PlaylistScreen from './PlaylistScreen.vue'
|
|||
let playlist: Playlist
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (songs: Song[]) {
|
||||
playlist = playlist || factory<Playlist>('playlist')
|
||||
playlistStore.init([playlist])
|
||||
playlist.songs = songs
|
||||
|
||||
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
|
||||
|
||||
const rendered = this.render(PlaylistScreen)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: `playlists/${playlist.id}`,
|
||||
screen: 'Playlist'
|
||||
}, { id: playlist.id })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(playlist, false))
|
||||
|
||||
return { rendered, fetchMock }
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders the playlist', async () => {
|
||||
await this.renderComponent(factory<Song>('song', 10))
|
||||
|
@ -75,4 +56,23 @@ new class extends UnitTestCase {
|
|||
expect(fetchMock).toHaveBeenCalledWith(playlist, true)
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (songs: Song[]) {
|
||||
playlist = playlist || factory<Playlist>('playlist')
|
||||
playlistStore.init([playlist])
|
||||
playlist.songs = songs
|
||||
|
||||
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
|
||||
|
||||
const rendered = this.render(PlaylistScreen)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: `playlists/${playlist.id}`,
|
||||
screen: 'Playlist'
|
||||
}, { id: playlist.id })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(playlist, false))
|
||||
|
||||
return { rendered, fetchMock }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ScreenBase v-if="playlist">
|
||||
<template #header>
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout" :disabled="loading">
|
||||
<ScreenHeader :disabled="loading" :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
{{ playlist.name }}
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls" />
|
||||
|
||||
|
@ -29,11 +29,11 @@
|
|||
<SongListControls
|
||||
v-if="!isPhone || showingControls"
|
||||
:config="controlsConfig"
|
||||
@delete-playlist="destroy"
|
||||
@filter="applyFilter"
|
||||
@refresh="fetchDetails(true)"
|
||||
@delete-playlist="destroy"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
@refresh="fetchDetails(true)"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
@ -44,11 +44,11 @@
|
|||
v-if="!loading && songs.length"
|
||||
ref="songList"
|
||||
class="-m-6"
|
||||
@reorder="onReorder"
|
||||
@sort="sort"
|
||||
@press:delete="removeSelected"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
@reorder="onReorder"
|
||||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
|
@ -78,12 +78,12 @@ import { eventBus, pluralize } from '@/utils'
|
|||
import { commonStore, playlistStore, songStore } from '@/stores'
|
||||
import { downloadService, playlistCollaborationService } from '@/services'
|
||||
import {
|
||||
useAuthorization,
|
||||
useErrorHandler,
|
||||
usePlaylistManagement,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useAuthorization,
|
||||
useSongListControls,
|
||||
useErrorHandler
|
||||
useSongListControls
|
||||
} from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
|
|
|
@ -7,18 +7,6 @@ import { playbackService } from '@/services'
|
|||
import QueueScreen from './QueueScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (songs: Song[]) {
|
||||
queueStore.state.songs = songs
|
||||
|
||||
this.render(QueueScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders the queue', () => {
|
||||
this.renderComponent(factory<Song>('song', 3))
|
||||
|
@ -57,4 +45,16 @@ new class extends UnitTestCase {
|
|||
await waitFor(() => expect(playMock).toHaveBeenCalledWith(songs, true))
|
||||
})
|
||||
}
|
||||
|
||||
private renderComponent (songs: Song[]) {
|
||||
queueStore.state.songs = songs
|
||||
|
||||
this.render(QueueScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,22 @@ import { screen, waitFor } from '@testing-library/vue'
|
|||
import RecentlyPlayedScreen from './RecentlyPlayedScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
it('displays the songs', async () => {
|
||||
await this.renderComponent(factory<Song>('song', 3))
|
||||
|
||||
screen.getByTestId('song-list')
|
||||
expect(screen.queryByTestId('screen-empty-state')).toBeNull()
|
||||
})
|
||||
|
||||
it('displays the empty state', async () => {
|
||||
await this.renderComponent([])
|
||||
|
||||
expect(screen.queryByTestId('song-list')).toBeNull()
|
||||
screen.getByTestId('screen-empty-state')
|
||||
})
|
||||
}
|
||||
|
||||
private async renderComponent (songs: Song[]) {
|
||||
recentlyPlayedStore.state.songs = songs
|
||||
const fetchMock = this.mock(recentlyPlayedStore, 'fetch')
|
||||
|
@ -22,20 +38,4 @@ new class extends UnitTestCase {
|
|||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('displays the songs', async () => {
|
||||
await this.renderComponent(factory<Song>('song', 3))
|
||||
|
||||
screen.getByTestId('song-list')
|
||||
expect(screen.queryByTestId('screen-empty-state')).toBeNull()
|
||||
})
|
||||
|
||||
it('displays the empty state', async () => {
|
||||
await this.renderComponent([])
|
||||
|
||||
expect(screen.queryByTestId('song-list')).toBeNull()
|
||||
screen.getByTestId('screen-empty-state')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,10 +44,10 @@
|
|||
or click here to select songs
|
||||
<input
|
||||
:accept="acceptAttribute"
|
||||
class="absolute opacity-0 w-full h-full z-[2] cursor-pointer left-0 top-0"
|
||||
multiple
|
||||
name="file[]"
|
||||
type="file"
|
||||
class="absolute opacity-0 w-full h-full z-[2] cursor-pointer left-0 top-0"
|
||||
@change="onFileInputChange"
|
||||
>
|
||||
</a>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue