quintodrome/ui/src/common/useToggleLove.test.js
Deluan Quintão 4172d2332a
feat(ui): add song Love and Rating functionality to playlist view (#4134)
* 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>
2025-06-04 20:38:28 -04:00

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' })
})
})
})