1
0

feat: play button, last played, loan lend/return on item page

This commit is contained in:
2026-09-03 22:45:20 +02:00
parent b2711b2b79
commit ece18d125b
4 changed files with 179 additions and 20 deletions

View File

@@ -3,19 +3,31 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import ItemPage from '../src/pages/ItemPage.js'
import type { Item } from '../src/types.js'
import { PlayerProvider } from '../src/player/PlayerContext.js'
import type { ItemDetail, MatchedAlbum } from '../src/types.js'
vi.mock('../src/api.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../src/api.js')>()
return {
...actual,
api: { ...actual.api, getItem: vi.fn(), setRip: vi.fn(), setMatch: vi.fn(), deleteItem: vi.fn(), searchAlbums: vi.fn() },
api: {
...actual.api,
getItem: vi.fn(),
setRip: vi.fn(),
setMatch: vi.fn(),
deleteItem: vi.fn(),
searchAlbums: vi.fn(),
getAlbumTracks: vi.fn(),
markPlayed: vi.fn(),
lendItem: vi.fn(),
returnLoan: vi.fn(),
},
}
})
import { api } from '../src/api.js'
const item: Item = {
const item: ItemDetail = {
id: 1,
discogsReleaseId: 1001,
title: 'Motion',
@@ -32,24 +44,37 @@ const item: Item = {
dateAdded: '2026-08-29',
ripOverride: null,
ripStatus: 'not_ripped',
matchedAlbum: null,
loan: null,
}
const matched: MatchedAlbum = { id: 77, subsonicId: 'alb-1', lastPlayedAt: '2026-09-01T10:00:00Z' }
const rippedItem: ItemDetail = { ...item, ripStatus: 'ripped', matchedAlbum: matched }
beforeEach(() => {
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue()
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockReturnValue(undefined)
vi.mocked(api.getItem).mockReset()
vi.mocked(api.getItem).mockResolvedValue(item as never)
vi.mocked(api.setRip).mockReset()
vi.mocked(api.setMatch).mockReset()
vi.mocked(api.deleteItem).mockReset()
vi.mocked(api.searchAlbums).mockReset()
vi.mocked(api.getAlbumTracks).mockReset()
vi.mocked(api.markPlayed).mockReset()
vi.mocked(api.lendItem).mockReset()
vi.mocked(api.returnLoan).mockReset()
})
function renderItem() {
return render(
<MemoryRouter initialEntries={['/item/1']}>
<Routes>
<Route path="/item/:id" element={<ItemPage />} />
<Route path="/library" element={<p>library</p>} />
</Routes>
<PlayerProvider>
<Routes>
<Route path="/item/:id" element={<ItemPage />} />
<Route path="/library" element={<p>library</p>} />
</Routes>
</PlayerProvider>
</MemoryRouter>
)
}
@@ -142,4 +167,43 @@ describe('ItemPage', () => {
await userEvent.click(await screen.findByRole('button', { name: /mark ripped/i }))
await waitFor(() => expect(screen.getByText(/didn't work/i)).toBeTruthy())
})
it('shows Play for a ripped item with a matched album and loads the player', async () => {
vi.mocked(api.getItem).mockResolvedValue(rippedItem as never)
vi.mocked(api.getAlbumTracks).mockResolvedValue({ id: 'alb-1', title: 'Motion', artist: 'TCO', tracks: [] } as never)
vi.mocked(api.markPlayed).mockResolvedValue({ ok: true } as never)
renderItem()
const play = await screen.findByRole('button', { name: /play album/i })
await userEvent.click(play)
await waitFor(() => expect(api.getAlbumTracks).toHaveBeenCalledWith('alb-1'))
await waitFor(() => expect(api.markPlayed).toHaveBeenCalledWith('alb-1'))
})
it('hides Play when unmatched or not ripped', async () => {
renderItem()
await waitFor(() => expect(screen.getByRole('heading', { name: 'Motion' })).toBeTruthy())
expect(screen.queryByRole('button', { name: /play album/i })).toBeNull()
})
it('shows last played under the rip banner', async () => {
vi.mocked(api.getItem).mockResolvedValue(rippedItem as never)
renderItem()
expect(await screen.findByText(/last played/i)).toBeTruthy()
})
it('lend and return flow', async () => {
vi.mocked(api.getItem)
.mockResolvedValueOnce(rippedItem as never)
.mockResolvedValueOnce({ ...rippedItem, loan: { id: 9, borrower: 'Bob', lentAt: '2026-09-03' } } as never)
vi.mocked(api.lendItem).mockResolvedValue({ id: 9, itemId: 1, borrower: 'Bob', lentAt: '2026-09-03', returnedAt: null } as never)
renderItem()
await userEvent.type(await screen.findByLabelText(/borrower/i), 'Bob')
await userEvent.click(screen.getByRole('button', { name: /^lend$/i }))
await waitFor(() => expect(api.lendItem).toHaveBeenCalledWith(1, 'Bob'))
vi.mocked(api.returnLoan).mockResolvedValue({ ok: true } as never)
await waitFor(() => expect(screen.getByText(/out to bob/i)).toBeTruthy())
await userEvent.click(screen.getByRole('button', { name: /mark returned/i }))
await waitFor(() => expect(api.returnLoan).toHaveBeenCalledWith(9))
})
})