test(frontend): strengthen useToggleLove assertions and add safety cases

- Tighten 'uses mediaFileId when present' to also assert the
  original-resource refresh (getOne called twice with both ids)
- Add success-path assertion that notify is not called when toggle
  succeeds
- Add error-path assertion that notify is not called when refresh fails
  after a successful toggle, and that loading still resets
- Add 'unmount safety' describe with a case asserting no React state
  update warnings when the toggle promise resolves after unmount

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yosoyepa 2026-05-15 16:50:20 -05:00
parent 4ce1ef0102
commit 58181cfca3

View File

@ -40,6 +40,8 @@ describe('useToggleLove', () => {
await result.current[0]()
})
expect(subsonic.star).toHaveBeenCalledWith('sg-1')
expect(getOne).toHaveBeenCalledTimes(2)
expect(getOne).toHaveBeenCalledWith('song', { id: 'pt-1' })
expect(getOne).toHaveBeenCalledWith('song', { id: 'sg-1' })
})
@ -62,6 +64,15 @@ describe('useToggleLove', () => {
expect(subsonic.unstar).toHaveBeenCalledWith('sg-1')
})
it('does not call notify on a successful toggle', async () => {
const record = { id: 'sg-1', starred: false }
const { result } = renderHook(() => useToggleLove('song', record))
await act(async () => {
await result.current[0]()
})
expect(mockNotify).not.toHaveBeenCalled()
})
describe('playlist track scenarios', () => {
it('refreshes both playlist track and song for playlist tracks', async () => {
const record = {
@ -159,6 +170,18 @@ describe('useToggleLove', () => {
})
expect(mockNotify).toHaveBeenCalledWith('ra.page.error', 'warning')
})
it('does not call notify when refresh fails after a successful toggle', async () => {
getOne.mockRejectedValue(new Error('refresh failed'))
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(mockNotify).not.toHaveBeenCalled()
expect(result.current[1]).toBe(false)
})
})
describe('loading state', () => {
@ -185,4 +208,37 @@ describe('useToggleLove', () => {
expect(result.current[1]).toBe(false)
})
})
describe('unmount safety', () => {
it('does not warn when the promise resolves after unmount', async () => {
let resolveStar
subsonic.star.mockReturnValue(
new Promise((r) => {
resolveStar = r
}),
)
const record = { id: 'sg-1', starred: false }
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const { result, unmount } = renderHook(() =>
useToggleLove('song', record),
)
act(() => {
result.current[0]()
})
unmount()
await act(async () => {
resolveStar()
})
const stateUpdateWarnings = errorSpy.mock.calls.filter(
([msg]) =>
typeof msg === 'string' &&
msg.includes("Can't perform a React state update"),
)
expect(stateUpdateWarnings).toHaveLength(0)
errorSpy.mockRestore()
})
})
})