* feat(ui): add playlist track love button Signed-off-by: Deluan <deluan@navidrome.org> * feat(ui): add star rating feature for playlist tracks Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): handle loading state and error logging in toggle love and rating components Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
136 lines
4.3 KiB
JavaScript
136 lines
4.3 KiB
JavaScript
import { renderHook, act } from '@testing-library/react-hooks'
|
|
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
import { useToggleLove } from './useToggleLove'
|
|
import subsonic from '../subsonic'
|
|
import { useDataProvider } from 'react-admin'
|
|
|
|
vi.mock('../subsonic', () => ({
|
|
default: {
|
|
star: vi.fn(() => Promise.resolve()),
|
|
unstar: vi.fn(() => Promise.resolve()),
|
|
},
|
|
}))
|
|
|
|
vi.mock('react-admin', async () => {
|
|
const actual = await vi.importActual('react-admin')
|
|
return {
|
|
...actual,
|
|
useDataProvider: vi.fn(),
|
|
useNotify: vi.fn(() => vi.fn()),
|
|
}
|
|
})
|
|
|
|
describe('useToggleLove', () => {
|
|
let getOne
|
|
beforeEach(() => {
|
|
getOne = vi.fn(() => Promise.resolve())
|
|
useDataProvider.mockReturnValue({ getOne })
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('uses mediaFileId when present', async () => {
|
|
const record = { id: 'pt-1', mediaFileId: 'sg-1', starred: false }
|
|
const { result } = renderHook(() => useToggleLove('song', record))
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
expect(subsonic.star).toHaveBeenCalledWith('sg-1')
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
|
|
})
|
|
|
|
it('falls back to id when mediaFileId not present', async () => {
|
|
const record = { id: 'sg-1', starred: false }
|
|
const { result } = renderHook(() => useToggleLove('song', record))
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
expect(subsonic.star).toHaveBeenCalledWith('sg-1')
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
|
|
})
|
|
|
|
it('calls unstar when record is already loved', async () => {
|
|
const record = { id: 'sg-1', starred: true }
|
|
const { result } = renderHook(() => useToggleLove('song', record))
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
expect(subsonic.unstar).toHaveBeenCalledWith('sg-1')
|
|
})
|
|
|
|
describe('playlist track scenarios', () => {
|
|
it('refreshes both playlist track and song for playlist tracks', async () => {
|
|
const record = {
|
|
id: 'pt-1',
|
|
mediaFileId: 'sg-1',
|
|
playlistId: 'pl-1',
|
|
starred: false,
|
|
}
|
|
const { result } = renderHook(() =>
|
|
useToggleLove('playlistTrack', record),
|
|
)
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
|
|
// Should star using the media file ID
|
|
expect(subsonic.star).toHaveBeenCalledWith('sg-1')
|
|
|
|
// Should refresh both the playlist track and the song
|
|
expect(getOne).toHaveBeenCalledTimes(2)
|
|
expect(getOne).toHaveBeenCalledWith('playlistTrack', {
|
|
id: 'pt-1',
|
|
filter: { playlist_id: 'pl-1' },
|
|
})
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
|
|
})
|
|
|
|
it('includes playlist_id filter when refreshing playlist tracks', async () => {
|
|
const record = {
|
|
id: 'pt-5',
|
|
mediaFileId: 'sg-10',
|
|
playlistId: 'pl-123',
|
|
starred: true,
|
|
}
|
|
const { result } = renderHook(() =>
|
|
useToggleLove('playlistTrack', record),
|
|
)
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
|
|
// Should unstar using the media file ID
|
|
expect(subsonic.unstar).toHaveBeenCalledWith('sg-10')
|
|
|
|
// Should refresh playlist track with correct playlist_id filter
|
|
expect(getOne).toHaveBeenCalledWith('playlistTrack', {
|
|
id: 'pt-5',
|
|
filter: { playlist_id: 'pl-123' },
|
|
})
|
|
// Should also refresh the underlying song
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-10' })
|
|
})
|
|
|
|
it('only refreshes original resource when no mediaFileId present', async () => {
|
|
const record = { id: 'sg-1', starred: false }
|
|
const { result } = renderHook(() => useToggleLove('song', record))
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
|
|
// Should only refresh the original resource (song)
|
|
expect(getOne).toHaveBeenCalledTimes(1)
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
|
|
})
|
|
|
|
it('does not include playlist_id filter for non-playlist resources', async () => {
|
|
const record = { id: 'sg-1', starred: false }
|
|
const { result } = renderHook(() => useToggleLove('song', record))
|
|
await act(async () => {
|
|
await result.current[0]()
|
|
})
|
|
|
|
// Should refresh without any filter
|
|
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
|
|
})
|
|
})
|
|
})
|